/*******************************************************************************
* Copyright (c) 2000, 2010 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
* Tim Hanson (thanson@bea.com) - patch for https://bugs.eclipse.org/bugs/show_bug.cgi?id=126673
*******************************************************************************/
package org.eclipse.jdt.internal.core;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import org.eclipse.core.internal.resources.Resource;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceStatus;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.content.IContentDescription;
import org.eclipse.jdt.core.BufferChangedEvent;
import org.eclipse.jdt.core.IBuffer;
import org.eclipse.jdt.core.IBufferChangedListener;
import org.eclipse.jdt.core.IJavaModelStatusConstants;
import org.eclipse.jdt.core.IOpenable;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.core.util.Util;
/**
* @see IBuffer
*
* @author Stas Negara - Added saving event notification to method save.
*/
public class Buffer implements IBuffer {
protected IFile file;
protected int flags;
protected char[] contents;
protected ArrayList changeListeners;
protected IOpenable owner;
protected int gapStart= -1;
protected int gapEnd= -1;
protected Object lock= new Object();
protected static final int F_HAS_UNSAVED_CHANGES= 1;
protected static final int F_IS_READ_ONLY= 2;
protected static final int F_IS_CLOSED= 4;
/**
* Creates a new buffer on an underlying resource.
*/
protected Buffer(IFile file, IOpenable owner, boolean readOnly) {
this.file= file;
this.owner= owner;
if (file == null) {
setReadOnly(readOnly);
}
}
/**
* @see IBuffer
*/
public synchronized void addBufferChangedListener(IBufferChangedListener listener) {
if (this.changeListeners == null) {
this.changeListeners= new ArrayList(5);
}
if (!this.changeListeners.contains(listener)) {
this.changeListeners.add(listener);
}
}
/**
* Append the <code>text</code> to the actual content, the gap is moved to the end of the
* <code>text</code>.
*/
public void append(char[] text) {
if (!isReadOnly()) {
if (text == null || text.length == 0) {
return;
}
int length= getLength();
synchronized (this.lock) {
if (this.contents == null)
return;
moveAndResizeGap(length, text.length);
System.arraycopy(text, 0, this.contents, length, text.length);
this.gapStart+= text.length;
this.flags|= F_HAS_UNSAVED_CHANGES;
}
notifyChanged(new BufferChangedEvent(this, length, 0, new String(text)));
}
}
/**
* Append the <code>text</code> to the actual content, the gap is moved to the end of the
* <code>text</code>.
*/
public void append(String text) {
if (text == null) {
return;
}
this.append(text.toCharArray());
}
/**
* @see IBuffer
*/
public void close() {
BufferChangedEvent event= null;
synchronized (this.lock) {
if (isClosed())
return;
event= new BufferChangedEvent(this, 0, 0, null);
this.contents= null;
this.flags|= F_IS_CLOSED;
}
notifyChanged(event); // notify outside of synchronized block
synchronized (this) { // ensure that no other thread is adding/removing a listener at the same time (https://bugs.eclipse.org/bugs/show_bug.cgi?id=126673)
this.changeListeners= null;
}
}
/**
* @see IBuffer
*/
public char getChar(int position) {
synchronized (this.lock) {
if (this.contents == null)
return Character.MIN_VALUE;
if (position < this.gapStart) {
return this.contents[position];
}
int gapLength= this.gapEnd - this.gapStart;
return this.contents[position + gapLength];
}
}
/**
* @see IBuffer
*/
public char[] getCharacters() {
synchronized (this.lock) {
if (this.contents == null)
return null;
if (this.gapStart < 0) {
return this.contents;
}
int length= this.contents.length;
char[] newContents= new char[length - this.gapEnd + this.gapStart];
System.arraycopy(this.contents, 0, newContents, 0, this.gapStart);
System.arraycopy(this.contents, this.gapEnd, newContents, this.gapStart, length - this.gapEnd);
return newContents;
}
}
/**
* @see IBuffer
*/
public String getContents() {
char[] chars= getCharacters();
if (chars == null)
return null;
return new String(chars);
}
/**
* @see IBuffer
*/
public int getLength() {
synchronized (this.lock) {
if (this.contents == null)
return -1;
int length= this.gapEnd - this.gapStart;
return (this.contents.length - length);
}
}
/**
* @see IBuffer
*/
public IOpenable getOwner() {
return this.owner;
}
/**
* @see IBuffer
*/
public String getText(int offset, int length) {
synchronized (this.lock) {
if (this.contents == null)
return ""; //$NON-NLS-1$
if (offset + length < this.gapStart)
return new String(this.contents, offset, length);
if (this.gapStart < offset) {
int gapLength= this.gapEnd - this.gapStart;
return new String(this.contents, offset + gapLength, length);
}
StringBuffer buf= new StringBuffer();
buf.append(this.contents, offset, this.gapStart - offset);
buf.append(this.contents, this.gapEnd, offset + length - this.gapStart);
return buf.toString();
}
}
/**
* @see IBuffer
*/
public IResource getUnderlyingResource() {
return this.file;
}
/**
* @see IBuffer
*/
public boolean hasUnsavedChanges() {
return (this.flags & F_HAS_UNSAVED_CHANGES) != 0;
}
/**
* @see IBuffer
*/
public boolean isClosed() {
return (this.flags & F_IS_CLOSED) != 0;
}
/**
* @see IBuffer
*/
public boolean isReadOnly() {
return (this.flags & F_IS_READ_ONLY) != 0;
}
/**
* Moves the gap to location and adjust its size to the anticipated change size. The size
* represents the expected range of the gap that will be filled after the gap has been moved.
* Thus the gap is resized to actual size + the specified size and moved to the given position.
*/
protected void moveAndResizeGap(int position, int size) {
char[] content= null;
int oldSize= this.gapEnd - this.gapStart;
if (size < 0) {
if (oldSize > 0) {
content= new char[this.contents.length - oldSize];
System.arraycopy(this.contents, 0, content, 0, this.gapStart);
System.arraycopy(this.contents, this.gapEnd, content, this.gapStart, content.length - this.gapStart);
this.contents= content;
}
this.gapStart= this.gapEnd= position;
return;
}
content= new char[this.contents.length + (size - oldSize)];
int newGapStart= position;
int newGapEnd= newGapStart + size;
if (oldSize == 0) {
System.arraycopy(this.contents, 0, content, 0, newGapStart);
System.arraycopy(this.contents, newGapStart, content, newGapEnd, content.length - newGapEnd);
} else if (newGapStart < this.gapStart) {
int delta= this.gapStart - newGapStart;
System.arraycopy(this.contents, 0, content, 0, newGapStart);
System.arraycopy(this.contents, newGapStart, content, newGapEnd, delta);
System.arraycopy(this.contents, this.gapEnd, content, newGapEnd + delta, this.contents.length - this.gapEnd);
} else {
int delta= newGapStart - this.gapStart;
System.arraycopy(this.contents, 0, content, 0, this.gapStart);
System.arraycopy(this.contents, this.gapEnd, content, this.gapStart, delta);
System.arraycopy(this.contents, this.gapEnd + delta, content, newGapEnd, content.length - newGapEnd);
}
this.contents= content;
this.gapStart= newGapStart;
this.gapEnd= newGapEnd;
}
/**
* Notify the listeners that this buffer has changed. To avoid deadlock, this should not be
* called in a synchronized block.
*/
protected void notifyChanged(final BufferChangedEvent event) {
ArrayList listeners= this.changeListeners;
if (listeners != null) {
for (int i= 0, size= listeners.size(); i < size; ++i) {
final IBufferChangedListener listener= (IBufferChangedListener)listeners.get(i);
SafeRunner.run(new ISafeRunnable() {
public void handleException(Throwable exception) {
Util.log(exception, "Exception occurred in listener of buffer change notification"); //$NON-NLS-1$
}
public void run() throws Exception {
listener.bufferChanged(event);
}
});
}
}
}
/**
* @see IBuffer
*/
public synchronized void removeBufferChangedListener(IBufferChangedListener listener) {
if (this.changeListeners != null) {
this.changeListeners.remove(listener);
if (this.changeListeners.size() == 0) {
this.changeListeners= null;
}
}
}
/**
* Replaces <code>length</code> characters starting from <code>position</code> with
* <code>text<code>.
* After that operation, the gap is placed at the end of the
* inserted <code>text</code>.
*/
public void replace(int position, int length, char[] text) {
if (!isReadOnly()) {
int textLength= text == null ? 0 : text.length;
synchronized (this.lock) {
if (this.contents == null)
return;
// move gap
moveAndResizeGap(position + length, textLength - length);
// overwrite
int min= Math.min(textLength, length);
if (min > 0) {
System.arraycopy(text, 0, this.contents, position, min);
}
if (length > textLength) {
// enlarge the gap
this.gapStart-= length - textLength;
} else if (textLength > length) {
// shrink gap
this.gapStart+= textLength - length;
System.arraycopy(text, 0, this.contents, position, textLength);
}
this.flags|= F_HAS_UNSAVED_CHANGES;
}
String string= null;
if (textLength > 0) {
string= new String(text);
}
notifyChanged(new BufferChangedEvent(this, position, length, string));
}
}
/**
* Replaces <code>length</code> characters starting from <code>position</code> with
* <code>text<code>.
* After that operation, the gap is placed at the end of the
* inserted <code>text</code>.
*/
public void replace(int position, int length, String text) {
this.replace(position, length, text == null ? null : text.toCharArray());
}
/**
* @see IBuffer
*/
public void save(IProgressMonitor progress, boolean force) throws JavaModelException {
// determine if saving is required
if (isReadOnly() || this.file == null) {
return;
}
if (!hasUnsavedChanges())
return;
//CODINGSPECTATOR: added variable 'success' and all code accessing it, and finally block
boolean success= false;
// use a platform operation to update the resource contents
try {
String stringContents= getContents();
if (stringContents == null)
return;
// Get encoding
String encoding= null;
try {
encoding= this.file.getCharset();
} catch (CoreException ce) {
// use no encoding
}
// Create bytes array
byte[] bytes= encoding == null
? stringContents.getBytes()
: stringContents.getBytes(encoding);
// Special case for UTF-8 BOM files
// see bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=110576
if (encoding != null && encoding.equals(org.eclipse.jdt.internal.compiler.util.Util.UTF_8)) {
IContentDescription description;
try {
description= this.file.getContentDescription();
} catch (CoreException e) {
if (e.getStatus().getCode() != IResourceStatus.RESOURCE_NOT_FOUND)
throw e;
// file no longer exist (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=234307 )
description= null;
}
if (description != null && description.getProperty(IContentDescription.BYTE_ORDER_MARK) != null) {
int bomLength= IContentDescription.BOM_UTF_8.length;
byte[] bytesWithBOM= new byte[bytes.length + bomLength];
System.arraycopy(IContentDescription.BOM_UTF_8, 0, bytesWithBOM, 0, bomLength);
System.arraycopy(bytes, 0, bytesWithBOM, bomLength, bytes.length);
bytes= bytesWithBOM;
}
}
// Set file contents
ByteArrayInputStream stream= new ByteArrayInputStream(bytes);
if (this.file.exists()) {
this.file.setContents(
stream,
force ? IResource.FORCE | IResource.KEEP_HISTORY : IResource.KEEP_HISTORY,
null);
} else {
this.file.create(stream, force, null);
}
success= true;
} catch (IOException e) {
throw new JavaModelException(e, IJavaModelStatusConstants.IO_EXCEPTION);
} catch (CoreException e) {
throw new JavaModelException(e);
} finally {
Resource.resourceListener.savedFile(file, success);
}
// the resource no longer has unsaved changes
this.flags&= ~(F_HAS_UNSAVED_CHANGES);
}
/**
* @see IBuffer
*/
public void setContents(char[] newContents) {
// allow special case for first initialization
// after creation by buffer factory
if (this.contents == null) {
synchronized (this.lock) {
this.contents= newContents;
this.flags&= ~(F_HAS_UNSAVED_CHANGES);
}
return;
}
if (!isReadOnly()) {
String string= null;
if (newContents != null) {
string= new String(newContents);
}
synchronized (this.lock) {
if (this.contents == null)
return; // ignore if buffer is closed (as per spec)
this.contents= newContents;
this.flags|= F_HAS_UNSAVED_CHANGES;
this.gapStart= -1;
this.gapEnd= -1;
}
BufferChangedEvent event= new BufferChangedEvent(this, 0, getLength(), string);
notifyChanged(event);
}
}
/**
* @see IBuffer
*/
public void setContents(String newContents) {
this.setContents(newContents.toCharArray());
}
/**
* Sets this <code>Buffer</code> to be read only.
*/
protected void setReadOnly(boolean readOnly) {
if (readOnly) {
this.flags|= F_IS_READ_ONLY;
} else {
this.flags&= ~(F_IS_READ_ONLY);
}
}
public String toString() {
StringBuffer buffer= new StringBuffer();
buffer.append("Owner: " + ((JavaElement)this.owner).toStringWithAncestors()); //$NON-NLS-1$
buffer.append("\nHas unsaved changes: " + hasUnsavedChanges()); //$NON-NLS-1$
buffer.append("\nIs readonly: " + isReadOnly()); //$NON-NLS-1$
buffer.append("\nIs closed: " + isClosed()); //$NON-NLS-1$
buffer.append("\nContents:\n"); //$NON-NLS-1$
char[] charContents= getCharacters();
if (charContents == null) {
buffer.append("<null>"); //$NON-NLS-1$
} else {
int length= charContents.length;
for (int i= 0; i < length; i++) {
char c= charContents[i];
switch (c) {
case '\n':
buffer.append("\\n\n"); //$NON-NLS-1$
break;
case '\r':
if (i < length - 1 && this.contents[i + 1] == '\n') {
buffer.append("\\r\\n\n"); //$NON-NLS-1$
i++;
} else {
buffer.append("\\r\n"); //$NON-NLS-1$
}
break;
default:
buffer.append(c);
break;
}
}
}
return buffer.toString();
}
}