InputStreamBufferpublic class InputStreamBuffer extends Object Wrapper for {@link InputStream} that allows you to read bytes from it like a byte[]. An
internal buffer is kept as small as possible to avoid large unnecessary allocations.
Care must be taken so that the internal buffer is kept small. The best practice is to
precalculate the maximum buffer size that you will need. For example,
say you have a loop that reads bytes from index 0 to 10 ,
skips to index N , reads from index N to N+10 , etc. Then
you know that the internal buffer can have a maximum size of 10 ,
and you should set the bufferSize parameter to 10 in the constructor.
Use {@link #advanceTo(int)} to declare that you will not need to access lesser indexes. This
helps to keep the internal buffer small. In the above example, after reading bytes from index
0 to 10 , you should call advanceTo(N) so that internal
buffer becomes filled with bytes from index N to N+10 .
If you know that you are reading bytes from a strictly increasing or equal
index, then you should set the autoAdvance parameter to true in the
constructor. For complicated access patterns, or when you prefer to control the internal
buffer yourself, set autoAdvance to false . When
autoAdvance is enabled, every time an index is beyond the buffer length,
the buffer will be shifted forward such that the index requested becomes the first element in
the buffer.
All public methods with parameter index are absolute indexed. The index is from
the beginning of the wrapped input stream. |
Fields Summary |
---|
private static final boolean | DEBUG | private static final int | DEBUG_MAX_BUFFER_SIZE | private static final String | TAG | private InputStream | mInputStream | private byte[] | mBuffer | private boolean | mAutoAdvance | private int | mOffsetByte count the buffer is offset by. | private int | mFilledNumber of bytes filled in the buffer. |
Constructors Summary |
---|
public InputStreamBuffer(InputStream inputStream, int bufferSize, boolean autoAdvance)Construct a new wrapper for an InputStream.
If autoAdvance is true, behavior is undefined if you call {@link #get(int)}
or {@link #has(int)} with an index N, then some arbitrary time later call {@link #get(int)}
or {@link #has(int)} with an index M < N. The wrapper may return the right value,
if the buffer happens to still contain index M, but more likely it will throw an
{@link IllegalStateException}.
If autoAdvance is false, you must be diligent and call {@link #advanceTo(int)}
at the appropriate times to ensure that the internal buffer is not unnecessarily resized
and reallocated.
mInputStream = inputStream;
if (bufferSize <= 0) {
throw new IllegalArgumentException(
String.format("Buffer size %d must be positive.", bufferSize));
}
bufferSize = leastPowerOf2(bufferSize);
mBuffer = new byte[bufferSize];
mAutoAdvance = autoAdvance;
|
Methods Summary |
---|
public void | advanceTo(int index)Attempts to advance the head of the buffer to the requested index. If the index is less
than the head of the buffer, the internal state will not be changed.
Advancing does not fill the internal buffer. The next {@link #get(int)} or
{@link #has(int)} call will fill the buffer.
Trace.beginSection("advance to");
final int i = index - mOffset;
if (i <= 0) {
// noop
Trace.endSection();
return;
} else if (i < mFilled) {
// Shift elements starting at i to position 0.
shiftToBeginning(i);
mOffset = index;
mFilled = mFilled - i;
} else if (mInputStream != null) {
// Burn some bytes from the input stream to match the new index.
int burn = i - mFilled;
boolean empty = false;
int fails = 0;
try {
while (burn > 0) {
final long burned = mInputStream.skip(burn);
if (burned <= 0) {
fails++;
} else {
burn -= burned;
}
if (fails >= 5) {
empty = true;
break;
}
}
} catch (IOException ignored) {
empty = true;
}
if (empty) {
//Mark input stream as consumed.
mInputStream = null;
}
mOffset = index - burn;
mFilled = 0;
} else {
// Advancing beyond the input stream.
mOffset = index;
mFilled = 0;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, String.format("advanceTo %d buffer: %s", i, this));
}
Trace.endSection();
| private boolean | fill(int index)Attempt to fill the internal buffer fully. The buffer will be modified such that the
requested index will always be in the buffer. If the index is less
than the head of the buffer, a runtime exception is thrown.
If the requested index is already in bounds of the buffer, then the buffer will just be
filled.
Otherwise, if autoAdvance was set to true in the constructor,
{@link #advanceTo(int)} will be called with the requested index,
and then the buffer filled. If autoAdvance was set to false,
we allocate a single larger buffer of a least multiple-of-two size that can contain the
requested index. The elements in the old buffer are copied over to the head of the new
buffer. Then the entire buffer is filled.
Trace.beginSection("fill");
if (index < mOffset) {
Trace.endSection();
throw new IllegalStateException(
String.format("Index %d is before buffer %d", index, mOffset));
}
int i = index - mOffset;
// Can't fill buffer anymore if input stream is consumed.
if (mInputStream == null) {
Trace.endSection();
return false;
}
// Increase buffer size if necessary.
int length = i + 1;
if (length > mBuffer.length) {
if (mAutoAdvance) {
advanceTo(index);
i = index - mOffset;
} else {
length = leastPowerOf2(length);
Log.w(TAG, String.format(
"Increasing buffer length from %d to %d. Bad buffer size chosen, "
+ "or advanceTo() not called.",
mBuffer.length, length));
mBuffer = Arrays.copyOf(mBuffer, length);
}
}
// Read from input stream to fill buffer.
int read = -1;
try {
read = mInputStream.read(mBuffer, mFilled, mBuffer.length - mFilled);
} catch (IOException ignored) {
}
if (read != -1) {
mFilled = mFilled + read;
} else {
// Mark input stream as consumed.
mInputStream = null;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, String.format("fill %d buffer: %s", i, this));
}
Trace.endSection();
return i < mFilled;
| public byte | get(int index)Attempt to get byte at the requested index from the wrapped input stream. If the internal
buffer contains the requested index, return immediately. If the index is less than the
head of the buffer, or the index is greater or equal to the size of the wrapped input stream,
a runtime exception is thrown.
If the index is not in the internal buffer, but it can be requested from the input stream,
{@link #fill(int)} will be called first, and the byte at the index returned.
You should always call {@link #has(int)} with the same index, unless you are sure that no
exceptions will be thrown as described above.
Consider calling {@link #advanceTo(int)} if you know that you will never request a lesser
index in the future.
Trace.beginSection("get");
if (has(index)) {
final int i = index - mOffset;
Trace.endSection();
return mBuffer[i];
} else {
Trace.endSection();
throw new IndexOutOfBoundsException(
String.format("Index %d beyond length.", index));
}
| public boolean | has(int index)Attempt to return whether the requested index is within the size of the wrapped input
stream. One side effect is {@link #fill(int)} will be called.
If this method returns true, it is guaranteed that {@link #get(int)} with the same index
will not fail. That means that if the requested index is within the size of the wrapped
input stream, but the index is less than the head of the internal buffer,
a runtime exception is thrown.
See {@link #get(int)} for caveats. A lot of the same warnings about exceptions and
advanceTo() apply.
Trace.beginSection("has");
if (index < mOffset) {
Trace.endSection();
throw new IllegalStateException(
String.format("Index %d is before buffer %d", index, mOffset));
}
final int i = index - mOffset;
// Requested index not in internal buffer.
if (i >= mFilled || i >= mBuffer.length) {
Trace.endSection();
return fill(index);
}
Trace.endSection();
return true;
| private static int | leastPowerOf2(int n)Calculate the least power of two greater than or equal to the input.
n--;
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
n |= n >> 8;
n |= n >> 16;
n++;
return n;
| private void | shiftToBeginning(int i)Modify the internal buffer so that all the bytes are shifted towards the head by
i . In other words, the byte at index i will now be at index
0 . Bytes from a lesser index are tossed.
if (i >= mBuffer.length) {
throw new IndexOutOfBoundsException(
String.format("Index %d out of bounds. Length %d", i, mBuffer.length));
}
for (int j = 0; j + i < mFilled; j++) {
mBuffer[j] = mBuffer[j + i];
}
| public java.lang.String | toDebugString()
Trace.beginSection("to debug string");
final StringBuilder sb = new StringBuilder();
sb.append("+").append(mOffset);
sb.append("+").append(mBuffer.length);
sb.append(" [");
for (int i = 0; i < mBuffer.length && i < DEBUG_MAX_BUFFER_SIZE; i++) {
if (i > 0) {
sb.append(",");
}
if (i < mFilled) {
sb.append(String.format("%02X", mBuffer[i]));
} else {
sb.append("__");
}
}
if (mInputStream != null) {
sb.append("...");
}
sb.append("]");
Trace.endSection();
return sb.toString();
| public java.lang.String | toString()
if (DEBUG) {
return toDebugString();
}
return String.format("+%d+%d [%d]", mOffset, mBuffer.length, mFilled);
|
|