blob: 080ac46459b7064b5b495e1eb1fdfc0b512e20fb [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2010 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.search.internal.core.text;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.content.IContentDescription;
import org.eclipse.core.resources.IFile;
/**
*
*/
public class FileCharSequenceProvider {
private static int NUMBER_OF_BUFFERS= 3;
public static int BUFFER_SIZE= 2 << 18; // public for testing
private FileCharSequence fReused= null;
public CharSequence newCharSequence(IFile file) throws CoreException, IOException {
if (fReused == null) {
return new FileCharSequence(file);
}
FileCharSequence curr= fReused;
fReused= null;
curr.reset(file);
return curr;
}
public void releaseCharSequence(CharSequence seq) throws IOException {
if (seq instanceof FileCharSequence) {
FileCharSequence curr= (FileCharSequence) seq;
try {
curr.close();
} finally {
if (fReused == null) {
fReused= curr;
}
}
}
}
public static class FileCharSequenceException extends RuntimeException {
private static final long serialVersionUID= 1L;
/* package */ FileCharSequenceException(IOException e) {
super(e);
}
/* package */ FileCharSequenceException(CoreException e) {
super(e);
}
public void throwWrappedException() throws CoreException, IOException {
Throwable wrapped= getCause();
if (wrapped instanceof CoreException) {
throw (CoreException) wrapped;
} else if (wrapped instanceof IOException) {
throw (IOException) wrapped;
}
// not possible
}
}
private static final class CharSubSequence implements CharSequence {
private final int fSequenceOffset;
private final int fSequenceLength;
private final FileCharSequence fParent;
public CharSubSequence(FileCharSequence parent, int offset, int length) {
fParent= parent;
fSequenceOffset= offset;
fSequenceLength= length;
}
@Override
public int length() {
return fSequenceLength;
}
@Override
public char charAt(int index) {
if (index < 0) {
throw new IndexOutOfBoundsException("index must be larger than 0"); //$NON-NLS-1$
}
if (index >= fSequenceLength) {
throw new IndexOutOfBoundsException("index must be smaller than length"); //$NON-NLS-1$
}
return fParent.charAt(fSequenceOffset + index);
}
@Override
public CharSequence subSequence(int start, int end) {
if (end < start) {
throw new IndexOutOfBoundsException("end cannot be smaller than start"); //$NON-NLS-1$
}
if (start < 0) {
throw new IndexOutOfBoundsException("start must be larger than 0"); //$NON-NLS-1$
}
if (end > fSequenceLength) {
throw new IndexOutOfBoundsException("end must be smaller or equal than length"); //$NON-NLS-1$
}
return fParent.subSequence(fSequenceOffset + start, fSequenceOffset + end);
}
@Override
public String toString() {
try {
return fParent.getSubstring(fSequenceOffset, fSequenceLength);
} catch (IOException e) {
throw new FileCharSequenceException(e);
} catch (CoreException e) {
throw new FileCharSequenceException(e);
}
}
}
private static final class Buffer {
private final char[] fBuf;
private int fOffset;
private int fLength;
private Buffer fNext;
private Buffer fPrevious;
public Buffer() {
fBuf= new char[BUFFER_SIZE];
reset();
fNext= this;
fPrevious= this;
}
public boolean contains(int pos) {
int offset= fOffset;
return offset <= pos && pos < offset + fLength;
}
/**
* Fills the buffer by reading from the given reader.
*
* @param reader the reader to read from
* @param pos the offset of the reader in the file
* @return returns true if the end of the file has been reached
* @throws IOException if reading from the buffer fails
*/
public boolean fill(Reader reader, int pos) throws IOException {
int res= reader.read(fBuf);
if (res == -1) {
fOffset= pos;
fLength= 0;
return true;
}
int charsRead= res;
while (charsRead < BUFFER_SIZE) {
res= reader.read(fBuf, charsRead, BUFFER_SIZE - charsRead);
if (res == -1) {
fOffset= pos;
fLength= charsRead;
return true;
}
charsRead+= res;
}
fOffset= pos;
fLength= BUFFER_SIZE;
return false;
}
public char get(int pos) {
return fBuf[pos - fOffset];
}
public StringBuilder append(StringBuilder buf, int start, int length) {
return buf.append(fBuf, start - fOffset, length);
}
public StringBuilder appendAll(StringBuilder buf) {
return buf.append(fBuf, 0, fLength);
}
public int getEndOffset() {
return fOffset + fLength;
}
public void removeFromChain() {
fPrevious.fNext= fNext;
fNext.fPrevious= fPrevious;
fNext= this;
fPrevious= this;
}
public void insertBefore(Buffer other) {
fNext= other;
fPrevious= other.fPrevious;
fPrevious.fNext= this;
other.fPrevious= this;
}
public Buffer getNext() {
return fNext;
}
public Buffer getPrevious() {
return fPrevious;
}
public void reset() {
fOffset= -1;
fLength= 0;
}
}
private final class FileCharSequence implements CharSequence {
private Reader fReader;
private int fReaderPos;
private Integer fLength;
private Buffer fMostCurrentBuffer; // access to the buffer chain
private int fNumberOfBuffers;
private IFile fFile;
public FileCharSequence(IFile file) throws CoreException, IOException {
fNumberOfBuffers= 0;
reset(file);
}
public void reset(IFile file) throws CoreException, IOException {
fFile= file;
fLength= null; // only calculated on demand
Buffer curr= fMostCurrentBuffer;
if (curr != null) {
do {
curr.reset();
curr= curr.getNext();
} while (curr != fMostCurrentBuffer);
}
initializeReader();
}
private void initializeReader() throws CoreException, IOException {
if (fReader != null) {
fReader.close();
}
String charset= fFile.getCharset();
fReader= new InputStreamReader(getInputStream(charset), charset);
fReaderPos= 0;
}
private InputStream getInputStream(String charset) throws CoreException, IOException {
boolean ok= false;
InputStream contents= fFile.getContents();
try {
if (StandardCharsets.UTF_8.name().equals(charset)) {
/*
* This is a workaround for a corresponding bug in Java readers and writer,
* see http://developer.java.sun.com/developer/bugParade/bugs/4508058.html
* we remove the BOM before passing the stream to the reader
*/
IContentDescription description= fFile.getContentDescription();
if ((description != null) && (description.getProperty(IContentDescription.BYTE_ORDER_MARK) != null)) {
int bomLength= IContentDescription.BOM_UTF_8.length;
byte[] bomStore= new byte[bomLength];
int bytesRead= 0;
do {
int bytes= contents.read(bomStore, bytesRead, bomLength - bytesRead);
if (bytes == -1)
throw new IOException();
bytesRead += bytes;
} while (bytesRead < bomLength);
if (!Arrays.equals(bomStore, IContentDescription.BOM_UTF_8)) {
// discard file reader, we were wrong, no BOM -> new stream
contents.close();
contents= fFile.getContents();
}
}
}
ok= true;
} finally {
if (!ok && contents != null)
try {
contents.close();
} catch (IOException ex) {
// ignore
}
}
return contents;
}
private void clearReader() throws IOException {
if (fReader != null) {
fReader.close();
}
fReader= null;
fReaderPos= Integer.MAX_VALUE;
}
@Override
public int length() {
if (fLength == null) {
try {
getBuffer(Integer.MAX_VALUE);
} catch (IOException e) {
throw new FileCharSequenceException(e);
} catch (CoreException e) {
throw new FileCharSequenceException(e);
}
}
return fLength.intValue();
}
private Buffer getBuffer(int pos) throws IOException, CoreException {
Buffer curr= fMostCurrentBuffer;
if (curr != null) {
do {
if (curr.contains(pos)) {
return curr;
}
curr= curr.getNext();
} while (curr != fMostCurrentBuffer);
}
Buffer buf= findBufferToUse();
fillBuffer(buf, pos);
if (buf.contains(pos)) {
return buf;
}
return null;
}
private Buffer findBufferToUse() {
if (fNumberOfBuffers < NUMBER_OF_BUFFERS) {
fNumberOfBuffers++;
Buffer newBuffer= new Buffer();
if (fMostCurrentBuffer == null) {
fMostCurrentBuffer= newBuffer;
return newBuffer;
}
newBuffer.insertBefore(fMostCurrentBuffer); // insert before first
return newBuffer;
}
return fMostCurrentBuffer.getPrevious();
}
private boolean fillBuffer(Buffer buffer, int pos) throws CoreException, IOException {
if (fReaderPos > pos) {
initializeReader();
}
do {
boolean endReached= buffer.fill(fReader, fReaderPos);
fReaderPos= buffer.getEndOffset();
if (endReached) {
fLength= Integer.valueOf(fReaderPos); // at least we know the size of the file now
fReaderPos= Integer.MAX_VALUE; // will have to reset next time
return true;
}
} while (fReaderPos <= pos);
return true;
}
@Override
public char charAt(final int index) {
final Buffer current= fMostCurrentBuffer;
if (current != null && current.contains(index)) {
return current.get(index);
}
if (index < 0) {
throw new IndexOutOfBoundsException("index must be larger than 0"); //$NON-NLS-1$
}
if (fLength != null && index >= fLength.intValue()) {
throw new IndexOutOfBoundsException("index must be smaller than length"); //$NON-NLS-1$
}
try {
final Buffer buffer= getBuffer(index);
if (buffer == null) {
throw new IndexOutOfBoundsException("index must be smaller than length"); //$NON-NLS-1$
}
if (buffer != fMostCurrentBuffer) {
// move to first
if (buffer.getNext() != fMostCurrentBuffer) { // already before the current?
buffer.removeFromChain();
buffer.insertBefore(fMostCurrentBuffer);
}
fMostCurrentBuffer= buffer;
}
return buffer.get(index);
} catch (IOException e) {
throw new FileCharSequenceException(e);
} catch (CoreException e) {
throw new FileCharSequenceException(e);
}
}
public String getSubstring(int start, int length) throws IOException, CoreException {
int pos= start;
int endPos= start + length;
if (fLength != null && endPos > fLength.intValue()) {
throw new IndexOutOfBoundsException("end must be smaller than length"); //$NON-NLS-1$
}
StringBuilder res= new StringBuilder(length);
Buffer buffer= getBuffer(pos);
while (pos < endPos && buffer != null) {
int bufEnd= buffer.getEndOffset();
if (bufEnd >= endPos) {
return buffer.append(res, pos, endPos - pos).toString();
}
buffer.append(res, pos, bufEnd - pos);
pos= bufEnd;
buffer= getBuffer(pos);
}
return res.toString();
}
@Override
public CharSequence subSequence(int start, int end) {
if (end < start) {
throw new IndexOutOfBoundsException("end cannot be smaller than start"); //$NON-NLS-1$
}
if (start < 0) {
throw new IndexOutOfBoundsException("start must be larger than 0"); //$NON-NLS-1$
}
if (fLength != null && end > fLength.intValue()) {
throw new IndexOutOfBoundsException("end must be smaller than length"); //$NON-NLS-1$
}
return new CharSubSequence(this, start, end - start);
}
public void close() throws IOException {
clearReader();
}
@Override
public String toString() {
int len= fLength != null ? fLength.intValue() : 4000;
StringBuilder res= new StringBuilder(len);
try {
Buffer buffer= getBuffer(0);
while (buffer != null) {
buffer.appendAll(res);
buffer= getBuffer(res.length());
}
return res.toString();
} catch (IOException e) {
throw new FileCharSequenceException(e);
} catch (CoreException e) {
throw new FileCharSequenceException(e);
}
}
}
}