/*
* Copyright 2009-2010 ZipXap Technology, LLC.
*
* Major refactoring by Simon Thiel
*
* 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
* or visit http://www.gnu.org/licenses/lgpl.html.
*/
package sit.sstl;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
/**
*
* Helper class for creating a large sequence of bytes. Think of it as the byte
* equivalent of StringBuilder
*
* <p>
* This class is part of the KavaPL base API.</p> <table>
* <tr><th>Milestone</th><th>Who</th><th>Date, Time</th></tr> <tr><td>Initial
* Version</td><td>K Penrose</td><td>June 24, 2007, 5:33 PM</td></tr> </table>
* <p>
** Note that this table is not intended as an exhaustive list of subtle
* changes. It is intended to list changes that break the compatibility of older
* code, or major enhancements/rewrites</p>
*
* 26/06/2012 - major refactoring by Simon Thiel
*/
public class ByteBuilder {
public static final int DEFAULT_CHUNK_SIZE = 4096;
/**
* non synchronized stream!!!
*/
public static class ByteBuilderOutputStream extends OutputStream {
private final ByteBuilder byteBuilder;
private final byte[] buffer;
private int bufferSize = 0;
public ByteBuilderOutputStream(ByteBuilder byteBuilder) {
this.byteBuilder = byteBuilder;
this.buffer = new byte[byteBuilder.getChunkSize()];
}
@Override
public void write(int aByte) throws IOException {
if (bufferSize >= buffer.length) {
appendBuffer();
}
buffer[bufferSize++] = (byte) aByte;
}
private void appendBuffer() {
byteBuilder.append(buffer, bufferSize);
bufferSize = 0;
}
@Override
public void flush() throws IOException {
super.flush();
appendBuffer();
}
@Override
public void close() throws IOException {
super.close();
appendBuffer();
}
}
protected final int chunkSize;
private class SequenceFinder {
private final byte[] searchSeq;
private int nextByteToFindIndex = 0;
private int index = 0;
private final int startIndex;
SequenceFinder(byte[] searchSeq, int startIndex) {
this.searchSeq = searchSeq;
this.startIndex = startIndex;
}
boolean handleNextByte(byte myByte) {
if ((index >= startIndex)
&& (myByte == searchSeq[nextByteToFindIndex])) {
nextByteToFindIndex++;
if (nextByteToFindIndex == searchSeq.length) {
return true;
}
} else {
nextByteToFindIndex = 0;
}
index++;
return false;
}
/**
* only valid in case handleNextByte returned true TODO better solution
* would be appreciated
*
* @return
*/
int getResult() {
return index - searchSeq.length + 1;
}
}
private final List<byte[]> bytesList = new ArrayList<byte[]>();
private int startIndexOfFirstChunk = 0;
private int fillIndex = 0;
private byte[] response = null;
public ByteBuilder(int chunkSize) {
this.chunkSize = chunkSize;
}
public ByteBuilder() {
chunkSize = DEFAULT_CHUNK_SIZE;
}
public ByteBuilder(byte[] bytes) {
chunkSize = bytes.length;
append(bytes);
}
public void clear() {
startIndexOfFirstChunk = 0;
fillIndex = 0;
bytesList.clear();
response = null;
}
public OutputStream getOutputStream() {
return new ByteBuilderOutputStream(this);
}
public int getChunkSize() {
return chunkSize;
}
public final void append(byte[] bytes) {
append(bytes, bytes.length);
}
/**
* appends the first 'bytesToAppend' bytes from 'bytes'
*
* @param bytes
* @param bytesToAppend
*/
public void append(byte[] bytes, int bytesToAppend) {
if (bytes == null) {
bytes = new byte[0];
}
if (bytesToAppend > bytes.length) {
throw new RuntimeException("bytesToAppend must be an integer less than or equal to bytes.length. "
+ "bytesToAppend=" + bytesToAppend + ", bytes.length=" + bytes.length);
}
byte[] newBytes;
response = null; //tell the toByteArray() to reconstruct the final response when called (dirty flag)
if (bytesList.isEmpty()) {
newBytes = new byte[chunkSize];
bytesList.add(newBytes);
fillIndex = 0;
} else {
newBytes = bytesList.get(bytesList.size() - 1);
}
for (int i = 0; i < bytesToAppend; i++) {
if (fillIndex == chunkSize) { //create and add new byte array
newBytes = new byte[chunkSize];
bytesList.add(newBytes);
fillIndex = 0;
}
newBytes[fillIndex] = bytes[i];
fillIndex++;
}
}
/**
* Discard the first <code>bytesToDiscard</code> bytes from the byte array.
*
* @param bytesToDiscard The number of bytes to discard from the byte array.
*/
public void discard(int bytesToDiscard) {
if (bytesToDiscard < 0 || bytesToDiscard > size()) {
throw new RuntimeException("bytesToDiscard must be an integer between 0 and size(). "
+ "bytesToDiscard=" + bytesToDiscard + ", size()=" + size());
}
response = null; //tell the toByteArray() to reconstruct the final response when called
while ((startIndexOfFirstChunk + bytesToDiscard) >= chunkSize) {
bytesList.remove(0);
bytesToDiscard -= chunkSize;
}
startIndexOfFirstChunk = startIndexOfFirstChunk + bytesToDiscard;
}
/**
* @return The number of bytes in the ByteBuilder.
*/
public int size() {
if (bytesList.isEmpty()) {
return 0;
}
return (bytesList.size() * chunkSize) - (chunkSize - fillIndex) - startIndexOfFirstChunk;
}
public byte[] toByteArray() {
if (response == null) {
if (size() == 0) { //nothing was ever appended
return new byte[0];
}//else
response = new byte[size()];
int index = 0;
for (int i = 0; i < bytesList.size(); i++) {
int myChunkSize = chunkSize;
if (i == bytesList.size() - 1) { //for the last chunk we must only read until fillindex
myChunkSize = fillIndex;
}
int myStartIndex = 0;
if (i == 0) { //for the first chunk start with startIndexOfFirstChunk
myStartIndex = startIndexOfFirstChunk;
}
byte[] curBytes = bytesList.get(i);
for (int j = myStartIndex; j < myChunkSize; j++) {
response[index++] = curBytes[j];
}
}
}
return response;
}
@Override
public String toString() {
//##CHARSET_MARKER##
return toString(Charset.defaultCharset());
}
public String toString(Charset charSet) {
//##CHARSET_MARKER##
return new String(toByteArray(), charSet);
}
/**
* Returns the index within this ByteArray of the first occurrence of the
* specified ByteArray.
*
* @param sequence
* @return if the argument occurs as a sequence within this object, then the
* index of the byte of the first such subsequence is returned; if it does
* not occur or in case sequence.length>size(), -1 is returned.
* @throws NullPointerException - if sequence is null.
*/
public int indexOf(byte[] sequence) {
return indexOf(0, sequence);
}
/**
* Returns the index within this ByteArray of the first occurrence of the
* specified ByteArray.
*
* @param sequence
* @param startIndex starting to search from (inclusive) startSearchIndex
* @return if the argument occurs as a sequence within this object, then the
* index of the byte of the first such subsequence is returned; if it does
* not occur or in case sequence.length>size(), -1 is returned.
* @throws NullPointerException - if sequence is null.
*/
public int indexOf(int startIndex, byte[] sequence) {
if (sequence.length > size()) {
return -1;
}
SequenceFinder sq = new SequenceFinder(sequence, startIndex);
for (int i = 0; i < bytesList.size(); i++) { //traverse the full chunks
byte[] curBytes = bytesList.get(i);
int myStartIndex = 0;
int myEndIndex = chunkSize;
if (i == 0) {
myStartIndex = startIndexOfFirstChunk;// skip startIndexOfFirstChunk bytes at the first chunk
}
if (i == bytesList.size() - 1) {
myEndIndex = fillIndex;
}
for (int j = myStartIndex; j < myEndIndex; j++) {
if (sq.handleNextByte(curBytes[j])) {
return sq.getResult();
}
}
}
return -1;
}
/**
*
* @param startIndex return subsequence starting at startIndex inclusive
* until the end of the ByteList
* @return
*/
public byte[] subSequence(int startIndex) {
return subSequence(startIndex, size());
}
/**
* @param startIndex return subsequence starting at startIndex inclusive
* @param endIndex returning sequence ends at endIndex exclusive - if
* endIndex>size() subsequence will be reduced to endIndex=size()
* @return
*/
public byte[] subSequence(int startIndex, int endIndex) {
byte[] result = new byte[endIndex - startIndex];
int index = 0;
int resIndex = 0;
for (int i = 0; i < bytesList.size(); i++) { //traverse all chunks
byte[] curBytes = bytesList.get(i);
int myStartIndex = 0;
int myEndIndex = chunkSize;
if (i == 0) {
myStartIndex = startIndexOfFirstChunk;// skip startIndexOfFirstChunk bytes at the first chunk
}
if (i == bytesList.size() - 1) {
myEndIndex = fillIndex;
}
for (int j = myStartIndex; j < myEndIndex; j++) {
if (index >= startIndex && index < endIndex) {
result[resIndex++] = curBytes[j];
}
index++;
}
}
return result;
}
}