// Copyright 2010-2011 Michel Kraemer
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// 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 de.undercouch.bson4jackson.io;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.CharBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
/**
* <p>A random-access buffer that resizes itself. This buffer differentiates
* from {@link java.io.ByteArrayOutputStream} in the following points:</p>
* <ul>
* <li>It allows specifying the byte order.</li>
* <li>It assigns several internal buffers instead of one and therefore
* saves garbage collection cycles.</li>
* <li>It is able to flush some of its internal buffers to an output stream
* or to a writable channel.</li>
* </ul>
* <p>The buffer has an initial size. This is also the size of each internal
* buffer, so if a new buffer has to be allocated it will take exactly
* that many bytes.</p>
* <p>By calling {@link #flushTo(OutputStream)} or {@link #flushTo(WritableByteChannel)}
* some of this buffer's internal buffers are flushed and then deallocated. The
* buffer maintains an internal counter for all flushed buffers. This allows the
* {@link #writeTo(OutputStream)} and {@link #writeTo(WritableByteChannel)}
* methods to only write non-flushed buffers. So, this class can be used for
* streaming by flushing internal buffers from time to time and at the end
* writing the rest:</p>
* <pre>
* ...
* buf.flushTo(out);
* ...
* buf.flushTo(out);
* ...
* buf.flushTo(out);
* ...
* buf.writeTo(out);</pre>
* <p>If flushing is never used a single call to one of the <code>writeTo</code>
* methods is enough to write the whole buffer.</p>
* <p>Once the buffer has been written to an output stream or channel, putting
* elements into it is not possible anymore and will lead to an
* {@link java.lang.IndexOutOfBoundsException}.</p>
* @author Michel Kraemer
*/
public class DynamicOutputBuffer {
/**
* The default byte order if nothing is specified
*/
public final static ByteOrder DEFAULT_BYTE_ORDER = ByteOrder.BIG_ENDIAN;
/**
* A unique key to make the first buffer re-usable
*/
protected static final StaticBuffers.Key BUFFER_KEY = StaticBuffers.Key.BUFFER2;
/**
* The default initial buffer size if nothing is specified
*/
public final static int DEFAULT_BUFFER_SIZE = Math.max(StaticBuffers.GLOBAL_MIN_SIZE, 1024 * 8);
/**
* The byte order of this buffer
*/
protected final ByteOrder _order;
/**
* The size of each internal buffer (also the initial buffer size)
*/
protected final int _bufferSize;
/**
* The current write position
*/
protected int _position;
/**
* The position of the first byte that has not been already
* flushed. Any attempt to put something into the buffer at
* a position before this first byte is invalid and causes
* a {@link IndexOutOfBoundsException} to be thrown.
*/
protected int _flushPosition;
/**
* The current buffer size (changes dynamically)
*/
protected int _size;
/**
* A linked list of internal buffers
*/
protected List<ByteBuffer> _buffers = new ArrayList<ByteBuffer>(1);
/**
* The character set used in {@link #putUTF8(String)}. Will be
* created lazily in {@link #getUTF8Charset()}
*/
protected Charset _utf8;
/**
* The encoder used in {@link #putUTF8(String)}. Will be created
* lazily in {@link #getUTF8Encoder()}
*/
protected CharsetEncoder _utf8Encoder;
/**
* A queue of buffers that have already been flushed and are
* free to reuse.
* @see #_reuseBuffersCount
*/
protected Queue<ByteBuffer> _buffersToReuse;
/**
* The number of buffers to reuse
* @see #_buffersToReuse
*/
protected int _reuseBuffersCount = 0;
/**
* Creates a dynamic buffer with BIG_ENDIAN byte order and
* a default initial buffer size of {@link #DEFAULT_BUFFER_SIZE} bytes.
*/
public DynamicOutputBuffer() {
this(DEFAULT_BYTE_ORDER);
}
/**
* Creates a dynamic buffer with BIG_ENDIAN byte order and
* the given initial buffer size.
* @param initialSize the initial buffer size
*/
public DynamicOutputBuffer(int initialSize) {
this(DEFAULT_BYTE_ORDER, initialSize);
}
/**
* Creates a dynamic buffer with the given byte order and
* a default initial buffer size of {@link #DEFAULT_BUFFER_SIZE} bytes.
* @param order the byte order
*/
public DynamicOutputBuffer(ByteOrder order) {
this(order, DEFAULT_BUFFER_SIZE);
}
/**
* Creates a dynamic buffer with the given byte order and
* the given initial buffer size.
* @param order the byte order
* @param initialSize the initial buffer size
*/
public DynamicOutputBuffer(ByteOrder order, int initialSize) {
if (initialSize <= 0) {
throw new IllegalArgumentException("Initial buffer size must be larger than 0");
}
_order = order;
_bufferSize = initialSize;
clear();
}
/**
* Sets the number of buffers to save for reuse after they have been
* invalidated by {@link #flushTo(OutputStream)} or {@link #flushTo(WritableByteChannel)}.
* Invalidated buffers will be saved in an internal queue. When the buffer
* needs a new internal buffer, it first attempts to reuse an existing one
* before allocating a new one.
* @param count the number of buffers to save for reuse
*/
public void setReuseBuffersCount(int count) {
_reuseBuffersCount = count;
if (_buffersToReuse != null) {
if (_reuseBuffersCount == 0) {
_buffersToReuse = null;
} else {
while (_reuseBuffersCount < _buffersToReuse.size()) {
_buffersToReuse.poll();
}
}
}
}
/**
* Allocates a new buffer or attempts to reuse an existing one.
* @return a new buffer with the current buffer size and the current byte order
*/
protected ByteBuffer allocateBuffer() {
if (_buffersToReuse != null && !_buffersToReuse.isEmpty()) {
ByteBuffer bb = _buffersToReuse.poll();
bb.rewind();
bb.limit(bb.capacity());
return bb;
}
ByteBuffer r = StaticBuffers.getInstance().byteBuffer(BUFFER_KEY, _bufferSize);
r.limit(_bufferSize);
return r.order(_order);
}
/**
* Removes a buffer from the list of internal buffers and saves it for
* reuse if this feature is enabled.
* @param n the number of the buffer to remove
*/
protected void deallocateBuffer(int n) {
ByteBuffer bb = _buffers.set(n, null);
if (bb != null && _reuseBuffersCount > 0) {
if (_buffersToReuse == null) {
_buffersToReuse = new LinkedList<ByteBuffer>();
}
if (_reuseBuffersCount > _buffersToReuse.size()) {
_buffersToReuse.add(bb);
}
}
}
/**
* Adds a new buffer to the list of internal buffers
* @return the new buffer
*/
protected ByteBuffer addNewBuffer() {
ByteBuffer bb = allocateBuffer();
_buffers.add(bb);
return bb;
}
/**
* Gets the buffer that holds the byte at the given absolute position.
* Automatically adds new internal buffers if the position lies outside
* the current range of all internal buffers.
* @param position the position
* @return the buffer at the requested position
*/
protected ByteBuffer getBuffer(int position) {
int n = position / _bufferSize;
while (n >= _buffers.size()) {
addNewBuffer();
}
return _buffers.get(n);
}
/**
* Adapts the buffer size so it is at least equal to the
* given number of bytes. This method does not add new
* internal buffers.
* @param size the minimum buffer size
*/
protected void adaptSize(int size) {
if (size > _size) {
_size = size;
}
}
/**
* @return the lazily created UTF-8 character set
*/
protected Charset getUTF8Charset() {
if (_utf8 == null) {
_utf8 = Charset.forName("UTF-8");;
}
return _utf8;
}
/**
* @return the lazily created UTF-8 encoder
*/
protected CharsetEncoder getUTF8Encoder() {
if (_utf8Encoder == null) {
_utf8Encoder = getUTF8Charset().newEncoder();
}
return _utf8Encoder;
}
/**
* @return the current buffer size (changes dynamically)
*/
public int size() {
return _size;
}
/**
* Clear the buffer and reset size and write position
*/
public void clear() {
//release a static buffer if possible
if (_buffersToReuse != null && !_buffersToReuse.isEmpty()) {
StaticBuffers.getInstance().releaseByteBuffer(BUFFER_KEY, _buffersToReuse.peek());
} else if (!_buffers.isEmpty()) {
StaticBuffers.getInstance().releaseByteBuffer(BUFFER_KEY, _buffers.get(0));
}
if (_buffersToReuse != null) {
_buffersToReuse.clear();
}
_buffers.clear();
_position = 0;
_flushPosition = 0;
_size = 0;
}
/**
* Puts a byte into the buffer at the current write position
* and increases the write position accordingly.
* @param b the byte to put
*/
public void putByte(byte b) {
putByte(_position, b);
++_position;
}
/**
* Puts several bytes into the buffer at the given position
* and increases the write position accordingly.
* @param bs an array of bytes to put
*/
public void putBytes(byte... bs) {
putBytes(_position, bs);
_position += bs.length;
}
/**
* Puts a byte into the buffer at the given position. Does
* not increase the write position.
* @param pos the position where to put the byte
* @param b the byte to put
*/
public void putByte(int pos, byte b) {
adaptSize(pos + 1);
ByteBuffer bb = getBuffer(pos);
int i = pos % _bufferSize;
bb.put(i, b);
}
/**
* Puts several bytes into the buffer at the given position.
* Does not increase the write position.
* @param pos the position where to put the bytes
* @param bs an array of bytes to put
*/
public void putBytes(int pos, byte... bs) {
adaptSize(pos + bs.length);
ByteBuffer bb = null;
int i = _bufferSize;
for (byte b : bs) {
if (i == _bufferSize) {
bb = getBuffer(pos);
i = pos % _bufferSize;
}
bb.put(i, b);
++i;
++pos;
}
}
/**
* Puts a 32-bit integer into the buffer at the current write position
* and increases write position accordingly.
* @param i the integer to put
*/
public void putInt(int i) {
putInt(_position, i);
_position += 4;
}
/**
* Puts a 32-bit integer into the buffer at the given position. Does
* not increase the write position.
* @param pos the position where to put the integer
* @param i the integer to put
*/
public void putInt(int pos, int i) {
adaptSize(pos + 4);
ByteBuffer bb = getBuffer(pos);
int index = pos % _bufferSize;
if (bb.limit() - index >= 4) {
bb.putInt(index, i);
} else {
byte b0 = (byte)i;
byte b1 = (byte)(i >> 8);
byte b2 = (byte)(i >> 16);
byte b3 = (byte)(i >> 24);
if (_order == ByteOrder.BIG_ENDIAN) {
putBytes(pos, b3, b2, b1, b0);
} else {
putBytes(pos, b0, b1, b2, b3);
}
}
}
/**
* Puts a 64-bit integer into the buffer at the current write position
* and increases the write position accordingly.
* @param l the 64-bit integer to put
*/
public void putLong(long l) {
putLong(_position, l);
_position += 8;
}
/**
* Puts a 64-bit integer into the buffer at the given position. Does
* not increase the write position.
* @param pos the position where to put the integer
* @param l the 64-bit integer to put
*/
public void putLong(int pos, long l) {
adaptSize(pos + 8);
ByteBuffer bb = getBuffer(pos);
int index = pos % _bufferSize;
if (bb.limit() - index >= 8) {
bb.putLong(index, l);
} else {
byte b0 = (byte)l;
byte b1 = (byte)(l >> 8);
byte b2 = (byte)(l >> 16);
byte b3 = (byte)(l >> 24);
byte b4 = (byte)(l >> 32);
byte b5 = (byte)(l >> 40);
byte b6 = (byte)(l >> 48);
byte b7 = (byte)(l >> 56);
if (_order == ByteOrder.BIG_ENDIAN) {
putBytes(pos, b7, b6, b5, b4, b3, b2, b1, b0);
} else {
putBytes(pos, b0, b1, b2, b3, b4, b5, b6, b7);
}
}
}
/**
* Puts a 32-bit floating point number into the buffer at the current
* write position and increases the write position accordingly.
* @param f the float to put
*/
public void putFloat(float f) {
putFloat(_position, f);
_position += 4;
}
/**
* Puts a 32-bit floating point number into the buffer at the given
* position. Does not increase the write position.
* @param pos the position where to put the float
* @param f the float to put
*/
public void putFloat(int pos, float f) {
putInt(pos, Float.floatToRawIntBits(f));
}
/**
* Puts a 64-bit floating point number into the buffer at the current
* write position and increases the write position accordingly.
* @param d the double to put
*/
public void putDouble(double d) {
putDouble(_position, d);
_position += 8;
}
/**
* Puts a 64-bit floating point number into the buffer at the given
* position. Does not increase the write position.
* @param pos the position where to put the double
* @param d the double to put
*/
public void putDouble(int pos, double d) {
putLong(pos, Double.doubleToRawLongBits(d));
}
/**
* Puts a character sequence into the buffer at the current
* write position and increases the write position accordingly.
* @param s the character sequence to put
*/
public void putString(CharSequence s) {
putString(_position, s);
_position += (s.length() * 2);
}
/**
* Puts a character sequence into the buffer at the given
* position. Does not increase the write position.
* @param pos the position where to put the character sequence
* @param s the character sequence to put
*/
public void putString(int pos, CharSequence s) {
for (int i = 0; i < s.length(); ++i) {
char c = s.charAt(i);
byte b0 = (byte)c;
byte b1 = (byte)(c >> 8);
if (_order == ByteOrder.BIG_ENDIAN) {
putBytes(pos, b1, b0);
} else {
putBytes(pos, b0, b1);
}
pos += 2;
}
}
/**
* Encodes the given string as UTF-8, puts it into the buffer
* and increases the write position accordingly.
* @param s the string to put
* @return the number of UTF-8 bytes put
*/
public int putUTF8(String s) {
int written = putUTF8(_position, s);
_position += written;
return written;
}
/**
* Puts the given string as UTF-8 into the buffer at the
* given position. This method does not increase the write position.
* @param pos the position where to put the string
* @param s the string to put
* @return the number of UTF-8 bytes put
*/
public int putUTF8(int pos, String s) {
ByteBuffer minibb = null;
CharsetEncoder enc = getUTF8Encoder();
CharBuffer in = CharBuffer.wrap(s);
int pos2 = pos;
ByteBuffer bb = getBuffer(pos2);
int index = pos2 % _bufferSize;
bb.position(index);
while (in.remaining() > 0) {
CoderResult res = enc.encode(in, bb, true);
//flush minibb first
if (bb == minibb) {
bb.flip();
while (bb.remaining() > 0) {
putByte(pos2, bb.get());
++pos2;
}
} else {
pos2 += bb.position() - index;
}
if (res.isOverflow()) {
if (bb.remaining() > 0) {
//exceeded buffer boundaries; write to a small temporary buffer
if (minibb == null) {
minibb = ByteBuffer.allocate(4);
}
minibb.rewind();
bb = minibb;
index = 0;
} else {
bb = getBuffer(pos2);
index = pos2 % _bufferSize;
bb.position(index);
}
} else if (res.isError()) {
try {
res.throwException();
} catch (CharacterCodingException e) {
throw new RuntimeException("Could not encode string", e);
}
}
}
adaptSize(pos2);
return pos2 - pos;
}
/**
* Tries to copy as much bytes as possible from this buffer to
* the given channel. See {@link #flushTo(WritableByteChannel)}
* for further information.
* @param out the output stream to write to
* @throws IOException if the buffer could not be flushed
*/
public void flushTo(OutputStream out) throws IOException {
int n1 = _flushPosition / _bufferSize;
int n2 = _position / _bufferSize;
if (n1 < n2) {
flushTo(Channels.newChannel(out));
}
}
/**
* Tries to copy as much bytes as possible from this buffer to
* the given channel. This method always copies whole internal
* buffers and deallocates them afterwards. It does not deallocate
* the buffer the write position is currently pointing to nor does
* it deallocate buffers following the write position. The method
* increases an internal pointer so consecutive calls also copy
* consecutive bytes.
* @param out the channel to write to
* @throws IOException if the buffer could not be flushed
*/
public void flushTo(WritableByteChannel out) throws IOException {
int n1 = _flushPosition / _bufferSize;
int n2 = _position / _bufferSize;
while (n1 < n2) {
ByteBuffer bb = _buffers.get(n1);
bb.rewind();
out.write(bb);
deallocateBuffer(n1);
_flushPosition += _bufferSize;
++n1;
}
}
/**
* Writes all non-flushed internal buffers to the given output
* stream. If {@link #flushTo(OutputStream)} has not been called
* before, this method writes the whole buffer to the output stream.
* @param out the output stream to write to
* @throws IOException if the buffer could not be written
*/
public void writeTo(OutputStream out) throws IOException {
writeTo(Channels.newChannel(out));
}
/**
* Writes all non-flushed internal buffers to the given channel.
* If {@link #flushTo(WritableByteChannel)} has not been called
* before, this method writes the whole buffer to the channel.
* @param out the channel to write to
* @throws IOException if the buffer could not be written
*/
public void writeTo(WritableByteChannel out) throws IOException {
int n1 = _flushPosition / _bufferSize;
int n2 = _buffers.size();
int toWrite = _size - _flushPosition;
while (n1 < n2) {
int curWrite = Math.min(toWrite, _bufferSize);
ByteBuffer bb = _buffers.get(n1);
bb.position(curWrite);
bb.flip();
out.write(bb);
++n1;
toWrite -= curWrite;
}
}
}