/***
**
** This library is free software; you can redistribute it and/or
** modify it under the terms of the GNU Lesser General Public
** License as published by the Free Software Foundation; either
** version 2.1 of the License, or (at your option) any later version.
**
** This library is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
** Lesser General Public License for more details.
**
** You should have received a copy of the GNU Lesser General Public
** License along with this library; if not, write to the Free Software
** Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
**
**/
package com.partydj.util;
import java.io.*;
import java.nio.*;
import java.nio.charset.*;
import sun.io.*;
public class ChunkedByteBuffer implements Serializable {
public static final int DEFAULT_CHUNK_SIZE = 4096;
public static final int DEFAULT_NUMBER_OF_CHUNKS = 16;
int chunkSize;
byte array[][];
int startingChunks;
int lastChunk;
int firstFree;
String charsetName = Charset.forName("ISO-8859-1").name(); // String for serialization
/**
* Create a new ChunkedByteBuffer with the passed incremental chunkSize
* and number of starting chunks. The number of starting chunks is mearly the
* size of the chunk holder array - which will have to grow whenever
* chunkSize * startingChunks of data is presented. At that time the growth is
* simply of the main array
*/
public ChunkedByteBuffer(int chunkSize, int startingChunks) {
//rather than assertions just fix bad arguments
startingChunks = Math.max(startingChunks, 1);
chunkSize = Math.max(chunkSize, 64);
lastChunk = 0;
firstFree = 0;
array = new byte[startingChunks][];
this.chunkSize = chunkSize;
this.startingChunks = startingChunks;
this.array[0] = new byte[chunkSize];
}
public ChunkedByteBuffer(int chunkSize) {
this(chunkSize, DEFAULT_NUMBER_OF_CHUNKS);
}
public ChunkedByteBuffer() {
this(DEFAULT_CHUNK_SIZE, DEFAULT_NUMBER_OF_CHUNKS);
}
public int size() {
return length();
}
public int length() {
return (lastChunk * chunkSize) + firstFree;
}
public int getChunkSize() {
return chunkSize;
}
public Charset getCharset() {
return Charset.forName(charsetName);
}
public void setCharset(Charset charset) {
if (charset != null) {
this.charsetName = charset.name();
}
}
/**
* remove the last byte appended to the buffer. if the buffer is empty, return -1
*/
public byte unappend() {
if (length() <= 0) {
return -1;
}
firstFree--;
if (firstFree < 0) {
firstFree = chunkSize - 1;
lastChunk--;
}
return array[lastChunk][firstFree];
}
/**
* remove the last n bytes appended to the buffer. if the buffer is empty, return an empty array;
*/
public byte[] unappend(int n) {
int length = length();
byte[] value = new byte[n < length ? n : length];
for (int i = value.length - 1; i >= 0 ; i--) {
firstFree--;
if (firstFree < 0) {
firstFree = chunkSize - 1;
lastChunk--;
}
value[i] = array[lastChunk][firstFree];
}
return value;
}
/**
* Append a single byte to the buffer - if there is space on the current
* chunk then whoopee this is easy - else regrow to make room
*/
public ChunkedByteBuffer append(byte value) {
//boolean lastchunk = lastChunk + 1 == array.length;
byte chunk[];
if (firstFree < chunkSize) {
chunk = array[lastChunk];
} else {
if (lastChunk + 1 == array.length) {
expandCapacity();
}
chunk = array[++lastChunk];
if (chunk == null) {
chunk = array[lastChunk] = new byte[chunkSize];
}
firstFree = 0;
}
chunk[firstFree++] = value;
return this;
}
/**
* Convenience method to append a String with default encoding to the buffer
*/
public ChunkedByteBuffer append(String value) throws UnsupportedEncodingException {
if (value != null && value.length() > 0) {
return append(value.getBytes(getCharset().name()));
} else {
return this;
}
}
/**
* Convenience method to append a String to the buffer
*/
public ChunkedByteBuffer append(String value, String enc) throws UnsupportedEncodingException {
if (value != null && value.length() > 0) {
return append(value.getBytes(enc));
} else {
return this;
}
}
/**
* Convenience method to append an entire byte[] area to the buffer
*/
public ChunkedByteBuffer append(byte bytes[]) {
if (bytes != null) {
return append(bytes, 0, bytes.length);
} else {
return this;
}
}
/**
* Append a byte[] area to the buffer - using System.arrayCopy we can move
* the array into the chunks directly
*/
public ChunkedByteBuffer append(byte bytes[], int start, int length) {
if (bytes != null && length > 0) {
int copyfrom = start;
byte chunk[] = array[lastChunk];
int available = chunkSize - firstFree;
while (length > 0) {
if (available > length) {
available = length;
}
System.arraycopy(bytes, copyfrom, array[lastChunk], firstFree, available);
length -= available;
copyfrom += available;
if (length > 0) {
if (lastChunk + 1 == array.length) {
expandCapacity();
}
chunk = array[++lastChunk];
if (chunk == null) {
chunk = array[lastChunk] = new byte[chunkSize];
}
available = chunkSize;
firstFree = 0;
}
}
firstFree += available;
}
return this;
}
/**
* Convenience method to append a java.nio ByteBuffer to the buffer
*/
public ChunkedByteBuffer append(ByteBuffer buf) {
if (buf != null && buf.remaining() > 0) {
int buflen = buf.remaining();
byte chunk[] = array[lastChunk];
int available = chunkSize - firstFree;
while (buflen > 0) {
if (available > buflen) {
available = buflen;
}
buf.get(array[lastChunk], firstFree, available);
buflen -= available;
if (buflen > 0) {
if (lastChunk + 1 == array.length) {
expandCapacity();
}
chunk = array[++lastChunk];
if (chunk == null) {
chunk = array[lastChunk] = new byte[chunkSize];
}
available = chunkSize;
firstFree = 0;
}
}
firstFree += available;
}
return this;
}
/**
* Read the contents of the InputStream into the buffer
*/
public ChunkedByteBuffer append(InputStream in) throws IOException {
return append(in, false);
}
/**
* Read the contents of the InputStream into the buffer
*/
public ChunkedByteBuffer append(InputStream in, boolean checkForAvailability) throws IOException {
if (in != null) {
int bytesRead = 0;
byte chunk[] = array[lastChunk];
int available = 0;
while (bytesRead != -1) {
available = chunkSize - firstFree;
bytesRead = !checkForAvailability || in.available() > 0 ? in.read(array[lastChunk], firstFree, available) : -1;
if (bytesRead > 0) {
if (available == bytesRead) {
if (lastChunk + 1 == array.length) {
expandCapacity();
}
chunk = array[++lastChunk];
if (chunk == null) {
chunk = array[lastChunk] = new byte[chunkSize];
}
available = chunkSize;
firstFree = 0;
} else {
firstFree += bytesRead;
}
}
}
}
return this;
}
/**
* Read the contents of ChunkedByteBuffer (using the ChunkedByteBuffer's inputstream) into the buffer
*/
public ChunkedByteBuffer append(ChunkedByteBuffer buffer) {
try {
return append(buffer.toInputStream());
} catch (IOException ioe) {
throw new RuntimeException("Error reading from passed ChunkedByteBuffer.toInputStream() source in to current chunkedbuffer.", ioe);
}
}
private void expandCapacity() {
//reconsider growth strategy
int i = array.length;
byte newarray[][] = new byte[(i + 1) * 2][];
System.arraycopy(array, 0, newarray, 0, i);
array = newarray;
}
/**
* Write the contents of the buffer to the OutputStream
*/
public void writeTo(OutputStream out) throws IOException {
int length = length();
int stopChunk = length / chunkSize;
int stopColumn = length % chunkSize;
for (int i = 0; i < stopChunk; i++) {
out.write(array[i], 0, chunkSize);
}
if (stopColumn > 0) {
out.write(array[stopChunk], 0, stopColumn);
}
}
public byte byteAt(int pos) {
if ((pos < 0) || (pos >= length())) {
throw new IndexOutOfBoundsException("Requested byteAt position " + pos + " is out of bounds on ChunkedByteBuffer");
}
int startChunk = pos / chunkSize;
return array[startChunk][pos % chunkSize];
}
/**
* Create a OutputStream with direct write access to the data in the buffer, this allows classes
* to write directly into the buffer via the OutputStream interface
*/
public OutputStream toOutputStream() {
return new OutputStream() {
boolean closed = false;
public void write(int b) throws IOException {
ensureOpen();
append((byte)b);
}
public void write(byte b[], int off, int len) throws IOException {
ensureOpen();
append(b, off, len);
}
public void close() throws IOException {
closed = true;
}
private void ensureOpen() throws IOException {
if (closed) {
throw new IOException("ChunkedByteBuffer OutputStream Closed by request");
}
}
};
}
/**
* Create a InputStream with access to the data in the buffer, this allows classes
* to read directly out of the buffer
*/
public InputStream toInputStream() {
return new InputStream() {
int index = 0;
int marked = 0;
boolean closed = false;
boolean eos = false;
private void ensureOpen() throws IOException {
if (closed) {
throw new IOException("ChunkedByteBuffer InputStream Closed by request");
}
}
public int read() throws IOException {
ensureOpen();
//already reached end-of-stream
if (eos) {
return -1;
}
int end = length();
//indicate the end-of-stream
if (index >= end) {
eos = true;
return -1;
}
byte buf[] = new byte[1];
getBytes(index / chunkSize, index % chunkSize, 1, buf, 0);
index ++;
return buf[0] & 0xff;
}
public int read(byte buf[]) throws IOException {
return read(buf, 0, buf.length);
}
public int read(byte buf[], int off, int len) throws IOException {
ensureOpen();
//already reached end-of-stream
if (eos) {
return -1;
}
int end = length();
//indicate the end-of-stream
if (index >= end) {
eos = true;
return -1;
}
len = Math.min(len, end - index);
getBytes(index / chunkSize, index % chunkSize, len, buf, off);
index += len;
return len;
}
public boolean ready() throws IOException {
ensureOpen();
return true;
}
public void close() throws IOException {
ensureOpen();
closed = true;
}
public void reset() throws IOException {
ensureOpen();
index = marked;
eos = false;
}
public void mark(int readAheadLimit) {
try {
ensureOpen();
marked = index;
} catch (IOException e) {
//no-op
}
}
public boolean markSupported() {
return true;
}
public long skip(int ns) {
if (index >= length()) {
return 0;
}
long n = Math.min(length() - index, ns);
index += n;
return n;
}
};
}
/**
* Return a the buffer as a byte array.
*/
public byte[] toByteArray() {
byte[] outBytes = new byte[size()];
getBytes(0, length(), outBytes, 0);
return outBytes;
}
/**
* return a 'substring' ChunkedByteBuffer.
*/
public ChunkedByteBuffer subBuffer(int beginIndex) {
return subBuffer(beginIndex, this.length());
}
/**
* return a 'substring' ChunkedByteBuffer.
*/
public ChunkedByteBuffer subBuffer(int beginIndex, int endIndex) {
return subBuffer(this.getChunkSize(), beginIndex, endIndex);
}
/**
* return a 'substring' ChunkedByteBuffer. This could possibly be made more efficient.
*/
public ChunkedByteBuffer subBuffer(int newChunkSize, int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new IndexOutOfBoundsException("Cannot create a sub buffer that starts before " + beginIndex + ".");
}
if (endIndex > this.length()) {
throw new IndexOutOfBoundsException("Cannot create a sub buffer that ends after " + endIndex + ".");
}
if (beginIndex > endIndex) {
throw new IndexOutOfBoundsException("Cannot create a sub buffer with a negative length.");
}
ChunkedByteBuffer newBuffer = new ChunkedByteBuffer(newChunkSize, ((endIndex - beginIndex) * 2) / newChunkSize);
int currentIndex = beginIndex;
//can this be done faster? maybe if newChunksize == this.getChunkSize() and if newChunkSize % beginIndex == 0?
while (currentIndex < endIndex) {
newBuffer.append(this.byteAt(currentIndex++));
}
return newBuffer;
}
/**
* get the chunk of the bytes which corresponds with the given index
*/
public byte[] getChunk(int i) {
return array[i];
}
/**
* Make a copy of the bytes from the buffer from the designated begining and length
* into the specified dest
*/
public int getBytes(int srcBegin, int srcEnd, byte[] dest, int destBegin) {
if (srcBegin < 0) {
throw new ArrayIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > length()) {
throw new ArrayIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new ArrayIndexOutOfBoundsException(srcEnd - srcBegin);
}
return getBytes(srcBegin / chunkSize, srcBegin % chunkSize, srcEnd - srcBegin, dest, destBegin);
}
private int getBytes(int startChunk, int startColumn, int length, byte[] dest, int destBegin) {
int stop = (startChunk * chunkSize) + startColumn + length;
int stopChunk = stop / chunkSize;
int stopColumn = stop % chunkSize;
int appended = 0;
for (int i = startChunk; i < stopChunk; i++) {
int size = chunkSize - startColumn;
System.arraycopy(array[i], startColumn, dest, destBegin, size);
destBegin = destBegin + size;
startColumn = 0;
appended += size;
}
if (stopColumn > 0) {
System.arraycopy(array[stopChunk], startColumn, dest, destBegin, stopColumn - startColumn);
appended += stopColumn - startColumn;
}
return appended;
}
/**
* Create a copy of the current data converted to a ChunkedCharBuffer
* the ChunkedCharBuffer need not necessarily be the same length as the
* byte buffer, depending on the ByteToChar conversion used
*
* @param enc The name of a supported character encoding
* @see ByteToCharConverter
*/
public ChunkedCharBuffer toChunkedCharBuffer(String enc) throws UnsupportedEncodingException {
return toChunkedCharBuffer(Charset.forName(enc));
}
/**
* Create a copy of the current data converted to a ChunkedCharBuffer
* the ChunkedCharBuffer need not necessarily be the same length as the
* byte buffer, depending on the ByteToChar conversion used
*
* @param charset The name of a supported character encoding
* @see ByteToCharConverter
*/
public ChunkedCharBuffer toChunkedCharBuffer(Charset charset) throws UnsupportedEncodingException {
try {
ChunkedCharBuffer charBuffer = new ChunkedCharBuffer(chunkSize, startingChunks);
charBuffer.setCharset(charset);
charBuffer.append(new InputStreamReader(toInputStream(), charset));
return charBuffer;
} catch (UnsupportedEncodingException e) {
throw e;
} catch (IOException e) {
throw new RuntimeException("Bug in ChunkedCharBuffer.append(Reader) or ChunkedByteBuffer.toInputStream()", e);
}
}
/**
* Create a copy of the current data converted to a ChunkedCharBuffer
* the ChunkedCharBuffer need not necessarily be the same length as the
* byte buffer, depending on the ByteToChar conversion used
*
* This method uses the default charset for this ChunkedByteBuffer
* @see ByteToCharConverter
*/
public ChunkedCharBuffer toChunkedCharBuffer() {
try {
return toChunkedCharBuffer(getCharset());
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UnsupportedEncoding " + charsetName, e);
}
}
/**
* Return a String representation of the byte buffer using the specified byte encoding
*
* @param enc The name of a supported character encoding
* @see ByteToCharConverter
*/
public String toString(String enc) throws UnsupportedEncodingException {
return toChunkedCharBuffer(enc).toString();
}
/**
* Return a String representation of the byte buffer using the default byte-to-char
* converter specified from ByteToCharConverter.getDefault()
*
* @see ByteToCharConverter
*/
@Override public String toString() {
return toChunkedCharBuffer().toString();
}
}