/* This file is part of VoltDB.
* Copyright (C) 2008-2010 VoltDB Inc.
*
* VoltDB is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VoltDB 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VoltDB. If not, see <http://www.gnu.org/licenses/>.
*/
package org.voltdb.messaging;
import java.io.DataOutput;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import org.voltdb.VoltType;
import org.voltdb.types.TimestampType;
import org.voltdb.types.VoltDecimalHelper;
import org.voltdb.utils.DBBPool;
import org.voltdb.utils.Encoder;
import org.voltdb.utils.DBBPool.BBContainer;
/**
* <code>DataInputStream</code> subclass to write objects that implement
* the FastSerializable interface.
*
*/
public class FastSerializer implements DataOutput {
/** callbacked when the internal buffer was grown. */
public interface BufferGrowCallback {
void onBufferGrow(FastSerializer obj);
}
public static final int INITIAL_ALLOCATION = 2048;
private BBContainer buffer;
private final BufferGrowCallback callback;
private final DBBPool m_pool;
private final boolean isDirect;
/**
* Create a <code>FastSerializer</code> that is BigEndian and uses a HeapByteBuffer
*/
public FastSerializer() {
this(true, false);
}
/**
* Create a FastSerializer that will use the provided pool for all of its allocations
* @param pool
*/
public FastSerializer(DBBPool pool) {
this(pool, INITIAL_ALLOCATION);
}
/**
* Create a FastSerializer that will use the provided pool for all its allocations
* with an initial allocation of the specified size
* @param pool
* @param initialAllocation
*/
public FastSerializer(DBBPool pool, int initialAllocation) {
this(true, false, null, pool, initialAllocation);
}
/** Warning: use direct ByteBuffers with caution, as they are generally slower. */
public FastSerializer(boolean bigEndian, boolean isDirect) {
this(bigEndian, isDirect, null, null, INITIAL_ALLOCATION);
}
/**
* Create an FS that will use the provided pool for direct allocations or
* will do its own direct allocations if the pool is null. The provided BufferGrowCallback will
* be invoked every time the buffer grows.
* @param bigEndian
* @param callback
* @param pool
*/
public FastSerializer(boolean bigEndian, BufferGrowCallback callback, DBBPool pool) {
this(bigEndian, pool == null ? true : false, callback, pool, INITIAL_ALLOCATION);
}
/**
* Caveat. A pool won't always give back a direct byte buffer. If a direct byte buffer
* is absolutely necessary for the serialized result then use isDirect and a null pool
*/
public FastSerializer(boolean bigEndian, boolean isDirect, BufferGrowCallback callback, DBBPool pool, int initialAllocation) {
assert(initialAllocation > 0);
assert(pool == null && isDirect || pool != null && !isDirect || pool == null && !isDirect);
this.isDirect = isDirect;
if (pool != null) {
m_pool = pool;
buffer = pool.acquire(initialAllocation);
} else if (isDirect) {
assert(pool == null);
m_pool = null;
buffer = DBBPool.allocateDirect(initialAllocation);
} else {
buffer = DBBPool.wrapBB(ByteBuffer.allocate(initialAllocation));
m_pool = null;
assert(pool == null);
}
this.callback = callback;
assert(buffer.b.order() == ByteOrder.BIG_ENDIAN);
}
public void reset() {
if (m_pool != null) {
buffer = m_pool.acquire(INITIAL_ALLOCATION);
} else if (this.isDirect) {
buffer = DBBPool.allocateDirect(INITIAL_ALLOCATION);
} else {
buffer = DBBPool.wrapBB(ByteBuffer.allocate(INITIAL_ALLOCATION));
}
}
public int size() {
return buffer.b.position();
}
/** Clears the contents of the underlying buffer, making it ready for more writes. */
public void clear() {
buffer.b.clear();
}
/** Resizes the internal byte buffer with a simple doubling policy, if needed. */
private final void growIfNeeded(int minimumDesired) {
if (buffer.b.remaining() < minimumDesired) {
// Compute the size of the new buffer
int newCapacity = buffer.b.capacity();
int newRemaining = newCapacity - buffer.b.position();
while (newRemaining < minimumDesired) {
newRemaining += newCapacity;
newCapacity *= 2;
}
// Allocate and copy
BBContainer next;
if (isDirect) {
next = DBBPool.allocateDirect(newCapacity);
} else if (m_pool != null) {
next = m_pool.acquire(newCapacity);
} else {
next = DBBPool.wrapBB(ByteBuffer.allocate(newCapacity));
}
buffer.b.flip();
next.b.put(buffer.b);
assert next.b.remaining() == newRemaining;
buffer.discard();
buffer = next;
if (callback != null) callback.onBufferGrow(this);
assert(buffer.b.order() == ByteOrder.BIG_ENDIAN);
}
}
/**
* Get the byte version of object. This is a shortcut utility method when
* you only need to serialize a single object.
*
* @return The byte array representation for <code>object</code>.
*/
public static byte[] serialize(FastSerializable object) throws IOException {
FastSerializer out = new FastSerializer();
object.writeExternal(out);
return out.getBBContainer().b.array();
}
public BBContainer writeObjectForMessaging(FastSerializable object) throws IOException {
final int startPosition = buffer.b.position();
buffer.b.putInt(0);
object.writeExternal(this);
final int len = buffer.b.position() - (4 + startPosition);
buffer.b.rewind();
buffer.b.putInt(len);
buffer.b.position(len + 4);
buffer.b.flip();
return buffer;
}
/** @return a reference to the underlying ByteBuffer. */
public BBContainer getBBContainer() {
buffer.b.flip();
return buffer;
}
/**
* This method is slow and horrible. It entails an extra copy. Don't use it! Ever! Not even for test!
* Just say no to test only code.
* It will also leak the BBContainer if this FS is being used with a pool. <<- Is that true??
*/
public byte[] getBytes() {
byte[] retval = new byte[buffer.b.position()];
int position = buffer.b.position();
buffer.b.rewind();
buffer.b.get(retval);
assert position == buffer.b.position();
return retval;
}
/**
* Return a readOnly slice of this buffer. Flips the internal buffer.
* May not be, usefully, invoked multiple times on the same internal
* state.
*
* Only use this if using a non-direct ByteBuffer!
*/
public ByteBuffer getBuffer() {
assert(m_pool == null);
assert(isDirect == false);
assert(buffer.b.hasArray());
assert(!buffer.b.isDirect());
buffer.b.flip();
return buffer.b.asReadOnlyBuffer();
}
/**
* When a fast serializer is shared between Java and native
* this is called to retrieve a reference to the to buffer without
* flipping it. OnBufferGrowCallback needs this to update the pointer
* to the shared buffer when the parameter buffer grows.
*/
public BBContainer getContainerNoFlip() {
assert(m_pool == null);
assert(isDirect == true);
assert(buffer.b.isDirect());
return buffer;
}
/**
* Get a ascii-string-safe version of the binary value using a
* hex encoding.
*
* @return A hex-encoded string value representing the serialized
* objects.
*/
public String getHexEncodedBytes() {
buffer.b.flip();
byte bytes[] = new byte[buffer.b.remaining()];
buffer.b.get(bytes);
String hex = Encoder.hexEncode(bytes);
buffer.discard();
return hex;
}
/**
* Write an object to the byte stream. Note: you need to know the
* type to read it back out again.
*
* @param obj The <code>FastSerializable</code> object to be written.
* @throws IOException Rethrows any IOExceptions thrown.
*/
public void writeObject(FastSerializable obj) throws IOException {
obj.writeExternal(this);
}
/**
* Write a timestamp to a FastSerializer. Store the value as a long
* representing microseconds from the epoch.
*
* @param timestamp The {@link org.voltdb.types.TimestampType TimestampType} to serialize.
* @throws IOException Rethrows any IOExceptions thrown.
*/
public void writeTimestamp(TimestampType timestamp) throws IOException {
assert timestamp != null;
long val = timestamp.getTime();
writeLong(val);
}
/**
* Write a string in the standard VoltDB way without
* wrapping the byte buffer.
*/
public static void writeString(String string, ByteBuffer buffer) throws IOException {
final int MAX_LENGTH = VoltType.MAX_VALUE_LENGTH;
final int NULL_STRING_INDICATOR = -1;
if (string == null) {
buffer.putInt(NULL_STRING_INDICATOR);
return;
}
int len = 0;
byte[] strbytes = {};
try {
strbytes = string.getBytes("UTF-8");
len = strbytes.length;
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
if (len > MAX_LENGTH) {
throw new IOException("String exceeds maximum length of "
+ MAX_LENGTH + " bytes.");
}
buffer.putInt(len);
buffer.put(strbytes);
}
/**
* Write a string in the standard VoltDB way. That is, two
* bytes of length info followed by the bytes of characters
* encoded in UTF-8.
*
* @param string The string value to be serialized.
* @throws IOException Rethrows any IOExceptions thrown.
*/
public void writeString(String string) throws IOException {
final int MAX_LENGTH = VoltType.MAX_VALUE_LENGTH;
final int NULL_STRING_INDICATOR = -1;
if (string == null) {
writeInt(NULL_STRING_INDICATOR);
return;
}
int len = 0;
byte[] strbytes = {};
try {
strbytes = string.getBytes("UTF-8");
len = strbytes.length;
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
if (len > MAX_LENGTH) {
throw new IOException("String exceeds maximum length of "
+ MAX_LENGTH + " bytes.");
}
writeInt(len);
write(strbytes);
}
// These writeArray() methods are tested in TestSQLTypesSuite.
// If changing the max limits, please update testInvalidParameterSerializations.
public void writeArray(FastSerializable[] values) throws IOException {
if (values.length > Short.MAX_VALUE) {
throw new IOException("Array exceeds maximum length of "
+ Short.MAX_VALUE + " bytes");
}
writeShort(values.length);
for (int i = 0; i < values.length; ++i) {
if (values[i] == null)
throw new IOException("Array being fastserialized can't contain null values (position " + i + ")");
writeObject(values[i]);
}
}
public void writeArray(boolean[] values) throws IOException {
if (values.length > VoltType.MAX_VALUE_LENGTH) {
throw new IOException("Array exceeds maximum length of "
+ VoltType.MAX_VALUE_LENGTH + " bytes");
}
writeInt(values.length);
for (int i = 0; i < values.length; ++i) {
writeBoolean(values[i]);
}
}
public void writeArray(byte[] values) throws IOException {
if (values.length > VoltType.MAX_VALUE_LENGTH) {
throw new IOException("Array exceeds maximum length of "
+ VoltType.MAX_VALUE_LENGTH + " bytes");
}
writeInt(values.length);
for (int i = 0; i < values.length; ++i) {
writeByte(values[i]);
}
}
public void writeArray(short[] values) throws IOException {
if (values.length > Short.MAX_VALUE) {
throw new IOException("Array exceeds maximum length of "
+ Short.MAX_VALUE + " bytes");
}
writeShort(values.length);
for (int i = 0; i < values.length; ++i) {
writeShort(values[i]);
}
}
public void writeArray(int[] values) throws IOException {
if (values.length > Short.MAX_VALUE) {
throw new IOException("Array exceeds maximum length of "
+ Short.MAX_VALUE + " bytes");
}
writeShort(values.length);
for (int i = 0; i < values.length; ++i) {
writeInt(values[i]);
}
}
public void writeArray(long[] values) throws IOException {
if (values.length > Short.MAX_VALUE) {
throw new IOException("Array exceeds maximum length of "
+ Short.MAX_VALUE + " bytes");
}
writeShort(values.length);
for (int i = 0; i < values.length; ++i) {
writeLong(values[i]);
}
}
public void writeArray(double[] values) throws IOException {
if (values.length > Short.MAX_VALUE) {
throw new IOException("Array exceeds maximum length of "
+ Short.MAX_VALUE + " bytes");
}
writeShort(values.length);
for (int i = 0; i < values.length; ++i) {
writeDouble(values[i]);
}
}
public void writeArray(String[] values) throws IOException {
if (values.length > Short.MAX_VALUE) {
throw new IOException("Array exceeds maximum length of "
+ Short.MAX_VALUE + " bytes");
}
writeShort(values.length);
for (int i = 0; i < values.length; ++i) {
writeString(values[i]);
}
}
public void writeArray(TimestampType[] values) throws IOException {
if (values.length > Short.MAX_VALUE) {
throw new IOException("Array exceeds maximum length of "
+ Short.MAX_VALUE + " bytes");
}
writeShort(values.length);
for (int i = 0; i < values.length; ++i) {
if (values[i] == null) writeLong(Long.MIN_VALUE);
else writeLong(values[i].getTime());
}
}
public void writeArray(BigDecimal[] values) throws IOException {
if (values.length > Short.MAX_VALUE) {
throw new IOException("Array exceeds maximum length of "
+ Short.MAX_VALUE + " bytes");
}
writeShort(values.length);
for (int i = 0; i < values.length; ++i) {
if (values[i] == null) {
VoltDecimalHelper.serializeNull(this);
}
else {
VoltDecimalHelper.serializeBigDecimal(values[i], this);
}
}
}
@Override
public void write(int b) throws IOException {
writeByte((byte) b);
}
@Override
public void write(byte[] b) throws IOException {
growIfNeeded(b.length);
buffer.b.put(b);
}
public void write(ByteBuffer b) throws IOException {
growIfNeeded(b.limit() - b.position());
buffer.b.put(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
growIfNeeded(len);
buffer.b.put(b, off, len);
}
@Override
public void writeBoolean(boolean v) throws IOException {
writeByte((byte) (v ? 1 : 0));
}
@Override
public void writeByte(int v) throws IOException {
growIfNeeded(Byte.SIZE/8);
buffer.b.put((byte) v);
}
@Override
public void writeBytes(String s) throws IOException {
throw new UnsupportedOperationException("FastSerializer.writeBytes() not supported.");
}
@Override
public void writeChar(int v) throws IOException {
growIfNeeded(Character.SIZE/8);
buffer.b.putChar((char) v);
}
@Override
public void writeChars(String s) throws IOException {
throw new UnsupportedOperationException("FastSerializer.writeChars() not supported.");
}
@Override
public void writeDouble(double v) throws IOException {
growIfNeeded(Double.SIZE/8);
buffer.b.putDouble(v);
}
@Override
public void writeFloat(float v) throws IOException {
growIfNeeded(Float.SIZE/8);
buffer.b.putFloat(v);
}
@Override
public void writeInt(int v) throws IOException {
growIfNeeded(Integer.SIZE/8);
buffer.b.putInt(v);
}
@Override
public void writeLong(long v) throws IOException {
growIfNeeded(Long.SIZE/8);
buffer.b.putLong(v);
}
@Override
public void writeShort(int v) throws IOException {
growIfNeeded(Short.SIZE/8);
buffer.b.putShort((short) v);
}
@Override
public void writeUTF(String str) throws IOException {
throw new UnsupportedOperationException("FastSerializer.writeChars() not supported.");
}
/**
* return Current position within the underlying buffer, for self-comparison only.
*/
public int getPosition() {
return buffer.b.position();
}
}