/* This file is part of VoltDB.
* Copyright (C) 2008-2014 VoltDB Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero 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.math.BigDecimal;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import org.voltcore.utils.DBBPool;
import org.voltcore.utils.DBBPool.BBContainer;
import org.voltdb.ParameterSet;
import org.voltdb.StoredProcedureInvocation;
import org.voltdb.VoltTable;
import org.voltdb.VoltType;
import org.voltdb.common.Constants;
import org.voltdb.types.TimestampType;
import org.voltdb.types.VoltDecimalHelper;
import org.voltdb.utils.Encoder;
import org.voltdb.utils.SerializationHelper;
/**
* <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 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 its allocations
* with an initial allocation of the specified size
* @param pool
* @param initialAllocation
*/
public FastSerializer(int initialAllocation) {
this(true, false, null, initialAllocation);
}
/** Warning: use direct ByteBuffers with caution, as they are generally slower. */
public FastSerializer(boolean bigEndian, boolean isDirect) {
this(bigEndian, isDirect, 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) {
this(bigEndian, true, callback, 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
*/
/** constructor that sets callback object. */
public FastSerializer(boolean bigEndian, boolean isDirect, BufferGrowCallback callback, int initialAllocation) {
assert(initialAllocation > 0);
this.isDirect = isDirect;
if (isDirect) {
buffer = DBBPool.allocateDirect(initialAllocation);
} else {
buffer = DBBPool.wrapBB(ByteBuffer.allocate(initialAllocation));
}
this.callback = callback;
buffer.b().order(bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN);
}
public int size() {
return buffer.b().position();
}
/** Clears the contents of the underlying buffer, making iteady 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 {
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();
}
/** @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.
*/
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(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(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 string in the standard VoltDB way without
* wrapping the byte buffer.
*/
public static void writeString(String string, ByteBuffer buffer) throws IOException {
if (string == null) {
buffer.putInt(VoltType.NULL_STRING_LENGTH);
return;
}
byte[] strbytes = string.getBytes(Constants.UTF8ENCODING);
int len = strbytes.length;
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 {
if (string == null) {
writeInt(VoltType.NULL_STRING_LENGTH);
return;
}
byte[] strbytes = string.getBytes(Constants.UTF8ENCODING);
int len = strbytes.length;
writeInt(len);
write(strbytes);
}
/**
* Write a varbinary in the standard VoltDB way. That is, four
* bytes of length info followed by the bytes.
*
* @param bin The byte array value to be serialized.
* @throws IOException Rethrows any IOExceptions thrown.
*/
public void writeVarbinary(byte[] bin) throws IOException {
if (bin == null) {
writeInt(VoltType.NULL_STRING_LENGTH);
return;
}
if (bin.length > VoltType.MAX_VALUE_LENGTH) {
throw new IOException("Varbinary exceeds maximum length of "
+ VoltType.MAX_VALUE_LENGTH + " bytes.");
}
writeInt(bin.length);
write(bin);
}
/**
* Write a table using it's ByteBuffer serialization code.
*/
public void writeTable(VoltTable table) throws IOException {
int len = table.getSerializedSize();
growIfNeeded(len);
table.flattenToBuffer(buffer.b());
}
/**
* Write an SPI using it's ByteBuffer serialization code.
*/
public void writeInvocation(StoredProcedureInvocation invocation) throws IOException {
int len = invocation.getSerializedSize();
growIfNeeded(len);
invocation.flattenToBuffer(buffer.b());
}
/**
* Write a ParameterSet using it's ByteBuffer serialization code.
*/
public void writeParameterSet(ParameterSet params) throws IOException {
int len = params.getSerializedSize();
growIfNeeded(len);
params.flattenToBuffer(buffer.b());
}
// These writeArray() methods are tested in TestSQLTypesSuite.
// If changing the max limits, please update testInvalidParameterSerializations.
public static void writeArray(VoltTable[] values, ByteBuffer buf) throws IOException {
if (values.length > Short.MAX_VALUE) {
throw new IOException("Array exceeds maximum length of "
+ Short.MAX_VALUE + " bytes");
}
buf.putShort((short)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 + ")");
values[i].flattenToBuffer(buf);
}
}
public static void writeArray(byte[][] values, ByteBuffer buf) throws IOException {
if (values.length > Short.MAX_VALUE) {
throw new IOException("Array exceeds maximum length of "
+ Short.MAX_VALUE + " bytes");
}
buf.putShort((short)values.length);
for (int i = 0; i < values.length; ++i) {
if (values[i] == null) {
buf.putInt(-1); // null length prefix
}
else {
SerializationHelper.writeArray(values[i], buf);
}
}
}
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(byte[][] values) throws IOException {
if (values.length > VoltType.MAX_VALUE_LENGTH) {
throw new IOException("Array exceeds maximum length of "
+ VoltType.MAX_VALUE_LENGTH + " bytes");
}
writeShort(values.length);
for (int i = 0; i < values.length; ++i) {
if (values[i] == null) {
writeInt(VoltType.NULL_STRING_LENGTH);
}
else {
writeArray(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);
write(values);
}
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);
growIfNeeded(16); // sizeof bigdecimal
for (int i = 0; i < values.length; ++i) {
if (values[i] == null) {
VoltDecimalHelper.serializeNull(buffer.b());
}
else {
VoltDecimalHelper.serializeBigDecimal(values[i], buffer.b());
}
}
}
@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();
}
/**
* Set current position of underlying buffer. Useful only in concert with getPosition()
* @param pos The position to set to.
*/
public void setPosition(int pos) {
buffer.b().position(pos);
}
public static void writeString(byte[] m_procNameBytes, ByteBuffer buf) throws IOException {
if (m_procNameBytes == null) {
buf.putInt(VoltType.NULL_STRING_LENGTH);
return;
}
buf.putInt(m_procNameBytes.length);
buf.put(m_procNameBytes);
}
public void discard() {
buffer.discard();
}
}