/* Jackson JSON-processor.
*
* Copyright (c) 2007- Tatu Saloranta, tatu.saloranta@iki.fi
*
* Licensed under the License specified in file LICENSE, included with
* the source code and binary code bundles.
* You may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.fasterxml.jackson.core.util;
import java.io.OutputStream;
import java.util.*;
/**
* Helper class that is similar to {@link java.io.ByteArrayOutputStream}
* in usage, but more geared to Jackson use cases internally.
* Specific changes include segment storage (no need to have linear
* backing buffer, can avoid reallocs, copying), as well API
* not based on {@link java.io.OutputStream}. In short, a very much
* specialized builder object.
*<p>
* Since version 1.5, also implements {@link OutputStream} to allow
* efficient aggregation of output content as a byte array, similar
* to how {@link java.io.ByteArrayOutputStream} works, but somewhat more
* efficiently for many use cases.
*/
public final class ByteArrayBuilder
extends OutputStream
{
private final static byte[] NO_BYTES = new byte[0];
/**
* Size of the first block we will allocate.
*/
private final static int INITIAL_BLOCK_SIZE = 500;
/**
* Maximum block size we will use for individual non-aggregated
* blocks. Let's limit to using 256k chunks.
*/
private final static int MAX_BLOCK_SIZE = (1 << 18);
final static int DEFAULT_BLOCK_ARRAY_SIZE = 40;
/**
* Optional buffer recycler instance that we can use for allocating
* the first block.
*/
private final BufferRecycler _bufferRecycler;
private final LinkedList<byte[]> _pastBlocks = new LinkedList<byte[]>();
/**
* Number of bytes within byte arrays in {@link _pastBlocks}.
*/
private int _pastLen;
private byte[] _currBlock;
private int _currBlockPtr;
public ByteArrayBuilder() { this(null); }
public ByteArrayBuilder(BufferRecycler br) { this(br, INITIAL_BLOCK_SIZE); }
public ByteArrayBuilder(int firstBlockSize) { this(null, firstBlockSize); }
public ByteArrayBuilder(BufferRecycler br, int firstBlockSize)
{
_bufferRecycler = br;
if (br == null) {
_currBlock = new byte[firstBlockSize];
} else {
_currBlock = br.allocByteBuffer(BufferRecycler.ByteBufferType.WRITE_CONCAT_BUFFER);
}
}
public void reset()
{
_pastLen = 0;
_currBlockPtr = 0;
if (!_pastBlocks.isEmpty()) {
_pastBlocks.clear();
}
}
/**
* Clean up method to call to release all buffers this object may be
* using. After calling the method, no other accessors can be used (and
* attempt to do so may result in an exception)
*/
public void release() {
reset();
if (_bufferRecycler != null && _currBlock != null) {
_bufferRecycler.releaseByteBuffer(BufferRecycler.ByteBufferType.WRITE_CONCAT_BUFFER, _currBlock);
_currBlock = null;
}
}
public void append(int i)
{
if (_currBlockPtr >= _currBlock.length) {
_allocMore();
}
_currBlock[_currBlockPtr++] = (byte) i;
}
public void appendTwoBytes(int b16)
{
if ((_currBlockPtr + 1) < _currBlock.length) {
_currBlock[_currBlockPtr++] = (byte) (b16 >> 8);
_currBlock[_currBlockPtr++] = (byte) b16;
} else {
append(b16 >> 8);
append(b16);
}
}
public void appendThreeBytes(int b24)
{
if ((_currBlockPtr + 2) < _currBlock.length) {
_currBlock[_currBlockPtr++] = (byte) (b24 >> 16);
_currBlock[_currBlockPtr++] = (byte) (b24 >> 8);
_currBlock[_currBlockPtr++] = (byte) b24;
} else {
append(b24 >> 16);
append(b24 >> 8);
append(b24);
}
}
/**
* Method called when results are finalized and we can get the
* full aggregated result buffer to return to the caller
*/
public byte[] toByteArray()
{
int totalLen = _pastLen + _currBlockPtr;
if (totalLen == 0) { // quick check: nothing aggregated?
return NO_BYTES;
}
byte[] result = new byte[totalLen];
int offset = 0;
for (byte[] block : _pastBlocks) {
int len = block.length;
System.arraycopy(block, 0, result, offset, len);
offset += len;
}
System.arraycopy(_currBlock, 0, result, offset, _currBlockPtr);
offset += _currBlockPtr;
if (offset != totalLen) { // just a sanity check
throw new RuntimeException("Internal error: total len assumed to be "+totalLen+", copied "+offset+" bytes");
}
// Let's only reset if there's sizable use, otherwise will get reset later on
if (!_pastBlocks.isEmpty()) {
reset();
}
return result;
}
/*
/**********************************************************
/* Non-stream API (similar to TextBuffer), since 1.6
/**********************************************************
*/
/**
* Method called when starting "manual" output: will clear out
* current state and return the first segment buffer to fill
*/
public byte[] resetAndGetFirstSegment() {
reset();
return _currBlock;
}
/**
* Method called when the current segment buffer is full; will
* append to current contents, allocate a new segment buffer
* and return it
*/
public byte[] finishCurrentSegment() {
_allocMore();
return _currBlock;
}
/**
* Method that will complete "manual" output process, coalesce
* content (if necessary) and return results as a contiguous buffer.
*
* @param lastBlockLength Amount of content in the current segment
* buffer.
*
* @return Coalesced contents
*/
public byte[] completeAndCoalesce(int lastBlockLength)
{
_currBlockPtr = lastBlockLength;
return toByteArray();
}
public byte[] getCurrentSegment() {
return _currBlock;
}
public void setCurrentSegmentLength(int len) {
_currBlockPtr = len;
}
public int getCurrentSegmentLength() {
return _currBlockPtr;
}
/*
/**********************************************************
/* OutputStream implementation
/**********************************************************
*/
@Override
public void write(byte[] b) {
write(b, 0, b.length);
}
@Override
public void write(byte[] b, int off, int len)
{
while (true) {
int max = _currBlock.length - _currBlockPtr;
int toCopy = Math.min(max, len);
if (toCopy > 0) {
System.arraycopy(b, off, _currBlock, _currBlockPtr, toCopy);
off += toCopy;
_currBlockPtr += toCopy;
len -= toCopy;
}
if (len <= 0) break;
_allocMore();
}
}
@Override
public void write(int b) {
append(b);
}
@Override public void close() { /* NOP */ }
@Override public void flush() { /* NOP */ }
/*
/**********************************************************
/* Internal methods
/**********************************************************
*/
private void _allocMore()
{
_pastLen += _currBlock.length;
/* Let's allocate block that's half the total size, except
* never smaller than twice the initial block size.
* The idea is just to grow with reasonable rate, to optimize
* between minimal number of chunks and minimal amount of
* wasted space.
*/
int newSize = Math.max((_pastLen >> 1), (INITIAL_BLOCK_SIZE + INITIAL_BLOCK_SIZE));
// plus not to exceed max we define...
if (newSize > MAX_BLOCK_SIZE) {
newSize = MAX_BLOCK_SIZE;
}
_pastBlocks.add(_currBlock);
_currBlock = new byte[newSize];
_currBlockPtr = 0;
}
}