package com.github.czyzby.websocket.serialization.impl; import java.io.UnsupportedEncodingException; import com.github.czyzby.websocket.serialization.SerializationException; import com.github.czyzby.websocket.serialization.Transferable; /** Object serializer that does not use reflection. Provides custom serialization that works on GWT. Java 6 and * GWT-compatible. Most methods return "this" for chaining. Not thread-safe. * * @author MJ */ public class Serializer { private static final int DEFAULT_BYTES_AMOUNT_ESTIMATION = 32; // Package private, as most serialization methods are in Size enum. int currentByteArrayIndex; byte[] serializedData; public Serializer() { this(DEFAULT_BYTES_AMOUNT_ESTIMATION); } /** @param estimatedBytesAmount will create an array of bytes using this size. If the serialized object will have * more bytes, the array will be resized. If it has less - the relevant bytes will be copied to a new, * shorter array upon final serializing method. Has to be positive. */ public Serializer(final int estimatedBytesAmount) { serializedData = new byte[estimatedBytesAmount]; } /** Changes current byte index, effectively using current wrapped byte array to serialize another object. */ public void reset() { currentByteArrayIndex = 0; } /** Resizes original byte array if the object turns out to be bigger than expected. * * @param bytesAmount amount of bytes that needs to be added to the array. */ void ensureCapacity(final int bytesAmount) { if (currentByteArrayIndex + bytesAmount >= serializedData.length) { // +1 ensures proper behavior for 0 estimate. int newSerializedDataArrayLength = (serializedData.length + 1) * 2; while (currentByteArrayIndex + bytesAmount >= newSerializedDataArrayLength) { newSerializedDataArrayLength = newSerializedDataArrayLength * 2; } final byte[] newSerializedDataArray = new byte[newSerializedDataArrayLength]; // Arrays.copyOf is not emulated on GWT. System.arraycopy(serializedData, 0, newSerializedDataArray, 0, currentByteArrayIndex); serializedData = newSerializedDataArray; } } /** @param value will be serialized with 1 byte. Custom, more efficient boolean serializations must be implemented * manually. * @return this (for chaining). */ public Serializer serializeBoolean(final boolean value) { ensureCapacity(Size.BYTE.getBytesAmount()); Size.BYTE.serializeBoolean(value, this); return this; } /** @param value will be serialized with 1 byte. * @return this (for chaining). */ public Serializer serializeByte(final byte value) { ensureCapacity(Size.BYTE.getBytesAmount()); Size.BYTE.serializeByte(value, this); return this; } /** @param value will be serialized with 2 bytes. * @return this (for chaining). */ public Serializer serializeShort(final short value) { return serializeShort(value, Size.SHORT); } /** @param value will be serialized. * @param size amount of bytes used to serialize the number. If smaller than actual number size, will truncate. If * bigger than actual number size, will ignore the size and serialize with the actual size needed to * store all bytes of number data. * @return this (for chaining). */ public Serializer serializeShort(final short value, final Size size) { ensureCapacity(size.getBytesAmount()); size.serializeShort(value, this); return this; } /** @param value will be serialized with 4 bytes. * @return this (for chaining). */ public Serializer serializeInt(final int value) { return serializeInt(value, Size.INT); } /** @param value will be serialized. * @param size amount of bytes used to serialize the number. If smaller than actual number size, will truncate. If * bigger than actual number size, will ignore the size and serialize with the actual size needed to * store all bytes of number data. * @return this (for chaining). */ public Serializer serializeInt(final int value, final Size size) { ensureCapacity(size.getBytesAmount()); size.serializeInt(value, this); return this; } /** @param value will be serialized with 8 bytes. * @return this (for chaining). */ public Serializer serializeLong(final long value) { return serializeLong(value, Size.LONG); } /** @param value will be serialized. * @param size amount of bytes used to serialize the number. If smaller than actual number size, will truncate. * @return this (for chaining). */ public Serializer serializeLong(final long value, final Size size) { ensureCapacity(size.getBytesAmount()); size.serializeLong(value, this); return this; } /** @param value will be serialized with 4 bytes. * @return this (for chaining). */ public Serializer serializeFloat(final float value) { try { return serializeFloat(value, Size.INT); } catch (final SerializationException exception) { // Should never happen, INT is big enough to store a float. throw new RuntimeException("Unexpected serialization exception.", exception); } } /** @param value will be serialized. * @param size amount of bytes used to serialize the number. * @throws SerializationException if the size is too small to store this number. * @return this (for chaining). */ public Serializer serializeFloat(final float value, final Size size) throws SerializationException { ensureCapacity(size.getBytesAmount()); size.serializeFloat(value, this); return this; } /** @param value will be serialized with 8 bytes. * @return this (for chaining). */ public Serializer serializeDouble(final double value) { try { return serializeDouble(value, Size.LONG); } catch (final SerializationException exception) { // Should never happen, LONG is big enough to store a double. throw new RuntimeException("Unexpected serialization exception.", exception); } } /** @param value will be serialized. * @param size amount of bytes used to serialize the number. If smaller than actual number size, will truncate. * @throws SerializationException if the size is too small to store this number. * @return this (for chaining). */ public Serializer serializeDouble(final double value, final Size size) throws SerializationException { ensureCapacity(size.getBytesAmount()); size.serializeDouble(value, this); return this; } /** @param value will serialize its ordinal number using 4 bytes. * @return this (for chaining). */ public Serializer serializeEnum(final Enum<?> value) { return serializeInt(value.ordinal()); } /** @param value will serialize its ordinal number. * @param enumLengthSize should be able to hold the biggest ordinal number of the passed enum. One byte is enough to * serialize most enums and in most applications there are hardly ever enough enum constants to surpass * two bytes length. * @return this (for chaining). */ public Serializer serializeEnum(final Enum<?> value, final Size enumLengthSize) { return serializeInt(value.ordinal(), enumLengthSize); } /** @param array will be serialized, using 1 byte to store each value and 4 bytes to store array size. Note that * more efficient implementations need to be custom. * @return this (for chaining). */ public Serializer serializeBooleanArray(final boolean[] array) { try { return serializeBooleanArray(array, Size.getDefaultArrayLengthSize()); } catch (final SerializationException exception) { // Should never happen. Default array length size is big enough to store any array size. throw new RuntimeException("Unexpected exception. Unable to serialize array.", exception); } } /** @param array will be serialized, using 1 byte to store each value. Note that more efficient implementations need * to be custom. * @param arrayLengthSize amount of bytes used to serialized array length. Array length cannot exceed * {@link Size#getMaxArrayLength()} . * @throws SerializationException if array length is longer than the maximum expected array length. * @return this (for chaining). */ public Serializer serializeBooleanArray(final boolean[] array, final Size arrayLengthSize) throws SerializationException { Size.BYTE.serializeBooleanArray(array, arrayLengthSize, this); return this; } /** @param array will be serialized, using 1 byte to store each value and 4 bytes to store array size. * @return this (for chaining). */ public Serializer serializeByteArray(final byte[] array) { try { return serializeByteArray(array, Size.getDefaultArrayLengthSize()); } catch (final SerializationException exception) { // Should never happen. Default array length size is big enough to store any array size. throw new RuntimeException("Unexpected exception. Unable to serialize array.", exception); } } /** @param array will be serialized, using 1 byte to store each value. * @param arrayLengthSize amount of bytes used to serialized array length. Array length cannot exceed * {@link Size#getMaxArrayLength()} . * @throws SerializationException if array length is longer than the maximum expected array length. * @return this (for chaining). */ public Serializer serializeByteArray(final byte[] array, final Size arrayLengthSize) throws SerializationException { Size.BYTE.serializeByteArray(array, arrayLengthSize, this); return this; } /** @param array will be serialized, using 2 bytes to store each value and 4 bytes to store array size. * @return this (for chaining). */ public Serializer serializeShortArray(final short[] array) { try { return serializeShortArray(array, Size.getDefaultArrayLengthSize()); } catch (final SerializationException exception) { // Should never happen. Default array length size is big enough to store any array size. throw new RuntimeException("Unexpected exception. Unable to serialize array.", exception); } } /** @param array will be serialized, using 2 bytes to store each value. * @param arrayLengthSize amount of bytes used to serialized array length. Array length cannot exceed * {@link Size#getMaxArrayLength()} . * @throws SerializationException if array length is longer than the maximum expected array length. * @return this (for chaining). */ public Serializer serializeShortArray(final short[] array, final Size arrayLengthSize) throws SerializationException { return serializeShortArray(array, arrayLengthSize, Size.SHORT); } /** @param array will be serialized. * @param arrayLengthSize amount of bytes used to serialized array length. Array length cannot exceed * {@link Size#getMaxArrayLength()} . * @param elementSize amount of bytes used to serialize each array element. If smaller than actual number size, will * truncate. If bigger than actual number size, will ignore the size and serialize with the actual size * needed to store all bytes of number data. * @throws SerializationException if array length is longer than the maximum expected array length. * @return this (for chaining). */ public Serializer serializeShortArray(final short[] array, final Size arrayLengthSize, final Size elementSize) throws SerializationException { elementSize.serializeShortArray(array, arrayLengthSize, this); return this; } /** @param array will be serialized, using 4 bytes to store each value and 4 bytes to store array size. * @return this (for chaining). */ public Serializer serializeIntArray(final int[] array) { try { return serializeIntArray(array, Size.getDefaultArrayLengthSize()); } catch (final SerializationException exception) { // Should never happen. Default array length size is big enough to store any array size. throw new RuntimeException("Unexpected exception. Unable to serialize array.", exception); } } /** @param array will be serialized, using 4 bytes to store each value. * @param arrayLengthSize amount of bytes used to serialized array length. Array length cannot exceed * {@link Size#getMaxArrayLength()} . * @throws SerializationException if array length is longer than the maximum expected array length. * @return this (for chaining). */ public Serializer serializeIntArray(final int[] array, final Size arrayLengthSize) throws SerializationException { return serializeIntArray(array, arrayLengthSize, Size.INT); } /** @param array will be serialized. * @param arrayLengthSize amount of bytes used to serialized array length. Array length cannot exceed * {@link Size#getMaxArrayLength()} . * @param elementSize amount of bytes used to serialize each array element. If smaller than actual number size, will * truncate. If bigger than actual number size, will ignore the size and serialize with the actual size * needed to store all bytes of number data. * @throws SerializationException if array length is longer than the maximum expected array length. * @return this (for chaining). */ public Serializer serializeIntArray(final int[] array, final Size arrayLengthSize, final Size elementSize) throws SerializationException { elementSize.serializeIntArray(array, arrayLengthSize, this); return this; } /** @param array will be serialized, using 8 bytes to store each value and 4 bytes to store array size. * @return this (for chaining). */ public Serializer serializeLongArray(final long[] array) { try { return serializeLongArray(array, Size.getDefaultArrayLengthSize()); } catch (final SerializationException exception) { // Should never happen. Default array length size is big enough to store any array size. throw new RuntimeException("Unexpected exception. Unable to serialize array.", exception); } } /** @param array will be serialized, using 8 bytes to store each value. * @param arrayLengthSize amount of bytes used to serialized array length. Array length cannot exceed * {@link Size#getMaxArrayLength()} . * @throws SerializationException if array length is longer than the maximum expected array length. * @return this (for chaining). */ public Serializer serializeLongArray(final long[] array, final Size arrayLengthSize) throws SerializationException { return serializeLongArray(array, arrayLengthSize, Size.LONG); } /** @param array will be serialized. * @param arrayLengthSize amount of bytes used to serialized array length. Array length cannot exceed * {@link Size#getMaxArrayLength()} . * @param elementSize amount of bytes used to serialize each array element. If smaller than actual number size, will * truncate. * @throws SerializationException if array length is longer than the maximum expected array length. * @return this (for chaining). */ public Serializer serializeLongArray(final long[] array, final Size arrayLengthSize, final Size elementSize) throws SerializationException { elementSize.serializeLongArray(array, arrayLengthSize, this); return this; } /** @param array will be serialized, using 4 bytes to store each value and 4 bytes to store array size. * @return this (for chaining). */ public Serializer serializeFloatArray(final float[] array) { try { return serializeFloatArray(array, Size.getDefaultArrayLengthSize()); } catch (final SerializationException exception) { // Should never happen. Default array length size is big enough to store any array size. throw new RuntimeException("Unexpected exception. Unable to serialize array.", exception); } } /** @param array will be serialized, using 4 bytes to store each value. * @param arrayLengthSize amount of bytes used to serialized array length. Array length cannot exceed * {@link Size#getMaxArrayLength()} . * @throws SerializationException if array length is longer than the maximum expected array length. * @return this (for chaining). */ public Serializer serializeFloatArray(final float[] array, final Size arrayLengthSize) throws SerializationException { return serializeFloatArray(array, arrayLengthSize, Size.INT); } /** @param array will be serialized. * @param arrayLengthSize amount of bytes used to serialized array length. Array length cannot exceed * {@link Size#getMaxArrayLength()} . * @param elementSize amount of bytes used to serialize each array element. * @throws SerializationException if array length is longer than the maximum expected array length or selected * element size is to small to store the number. * @return this (for chaining). */ public Serializer serializeFloatArray(final float[] array, final Size arrayLengthSize, final Size elementSize) throws SerializationException { elementSize.serializeFloatArray(array, arrayLengthSize, this); return this; } /** @param array will be serialized, using 8 bytes to store each value and 4 bytes to store array size. * @return this (for chaining). */ public Serializer serializeDoubleArray(final double[] array) { try { return serializeDoubleArray(array, Size.getDefaultArrayLengthSize()); } catch (final SerializationException exception) { // Should never happen. Default array length size is big enough to store any array size. throw new RuntimeException("Unexpected exception. Unable to serialize array.", exception); } } /** @param array will be serialized, using 8 bytes to store each value. * @param arrayLengthSize amount of bytes used to serialized array length. Array length cannot exceed * {@link Size#getMaxArrayLength()} . * @throws SerializationException if array length is longer than the maximum expected array length. * @return this (for chaining). */ public Serializer serializeDoubleArray(final double[] array, final Size arrayLengthSize) throws SerializationException { return serializeDoubleArray(array, arrayLengthSize, Size.LONG); } /** @param array will be serialized. * @param arrayLengthSize amount of bytes used to serialized array length. Array length cannot exceed * {@link Size#getMaxArrayLength()} . * @param elementSize amount of bytes used to serialize each array element. If smaller than actual number size, will * truncate. * @throws SerializationException if array length is longer than the maximum expected array length or selected * element size is to small to store the number. * @return this (for chaining). */ public Serializer serializeDoubleArray(final double[] array, final Size arrayLengthSize, final Size elementSize) throws SerializationException { elementSize.serializeDoubleArray(array, arrayLengthSize, this); return this; } /** @param value will be serialized as byte array with 4 bytes to store array length. * @return this (for chaining). */ public Serializer serializeString(final String value) { final byte[] stringAsBytes = value == null ? null : value.getBytes(); return serializeByteArray(stringAsBytes); } /** @param value will be serialized as byte array. * @param stringLengthSize estimated size needed to store maximum amount of bytes needed to store the string. Each * character translates roughly to 1 byte. * @throws SerializationException if string's byte array is longer than the estimated max size. * @return this (for chaining). */ public Serializer serializeString(final String value, final Size stringLengthSize) throws SerializationException { byte[] stringAsBytes; try { stringAsBytes = value == null ? null : value.getBytes("UTF-8"); } catch (final UnsupportedEncodingException exception) { throw new SerializationException("Unexpected: UTF-8 not supported.", exception); } return serializeByteArray(stringAsBytes, stringLengthSize); } /** @param array will be serialized as array of byte arrays using 4 bytes to store byte array lengths and 4 bytes to * store main array length. * @return this (for chaining). */ public Serializer serializeStringArray(final String[] array) { try { return serializeStringArray(array, Size.getDefaultArrayLengthSize(), Size.getDefaultArrayLengthSize()); } catch (final SerializationException exception) { // Should never happen. Array length sizes should be able to store any string arrays. throw new RuntimeException("Unexpected exception. Unable to serialize string array.", exception); } } /** @param array will be serialized as array of byte arrays using 4 bytes to store each byte array length. * @param arrayLengthSize estimated maximum amount of bytes needed to store array's length. Array length cannot * exceed {@link Size#getMaxArrayLength()} * @throws SerializationException if string's byte array is longer than the estimated max size. * @return this (for chaining). */ public Serializer serializeStringArray(final String[] array, final Size arrayLengthSize) throws SerializationException { return serializeStringArray(array, arrayLengthSize, Size.getDefaultArrayLengthSize()); } /** @param array will be serialized as array of byte arrays. * @param arrayLengthSize estimated maximum amount of bytes needed to store array's length. Array length cannot * exceed {@link Size#getMaxArrayLength()} . * @param stringLengthSize estimated size needed to store maximum amount of bytes needed to store each string. Each * character translates roughly to 1 byte. * @throws SerializationException if string's byte array is longer than the estimated max size or any of the strings * is too long for the given string length size. * @return this (for chaining). */ public Serializer serializeStringArray(final String[] array, final Size arrayLengthSize, final Size stringLengthSize) throws SerializationException { if (array == null) { serializeInt(Size.NULL_ARRAY_ID, arrayLengthSize); return this; } arrayLengthSize.validateArrayLengthToSerialize(array.length); serializeInt(array.length, arrayLengthSize); for (final String value : array) { serializeString(value, stringLengthSize); } return this; } /** @param array will be serialized as array of byte arrays. * @param arrayLengthSize estimated maximum amount of bytes needed to store array's length. Array length cannot * exceed {@link Size#getMaxArrayLength()} . * @param stringLengthSize estimated size needed to store maximum amount of bytes needed to store each string. Each * character translates roughly to 1 byte. * @param start first index from which strings should be serialized. * @param count amount of strings to serialize. * @throws SerializationException if string's byte array is longer than the estimated max size or any of the strings * is too long for the given string length size. * @return this (for chaining). */ public Serializer serializeStringArray(final String[] array, final Size arrayLengthSize, final Size stringLengthSize, final int start, final int count) throws SerializationException { if (array == null) { serializeInt(Size.NULL_ARRAY_ID, arrayLengthSize); return this; } final int length = start + count; arrayLengthSize.validateArrayLengthToSerialize(length); serializeInt(length, arrayLengthSize); for (int index = start; index < length; index++) { serializeString(array[index], stringLengthSize); } return this; } /** @param transferable will be serialized. Cannot be null. * @throws SerializationException if unable to serialize object or the object is null. * @return this (for chaining). */ public Serializer serializeTransferable(final Transferable<?> transferable) throws SerializationException { if (transferable == null) { throw new SerializationException("Cannot serialize transferable: null object received.", new NullPointerException()); } transferable.serialize(this); return this; } /** @param transferables will be serialized using 4 bytes to store array length. None of the transferables can be * null. * @throws SerializationException if unable to serialize any of the transferables. * @return this (for chaining). */ public Serializer serializeTransferableArray(final Transferable<?>[] transferables) throws SerializationException { return serializeTransferableArray(transferables, 0, transferables.length, Size.getDefaultArrayLengthSize()); } /** @param transferables will be serialized. None of the transferables can be null. * @param arrayLengthSize estimated amount of bytes needed to store length of the array. * @throws SerializationException is array length size is too small to store array's length or if unable to * serialize any of the transferables. * @return this (for chaining). */ public Serializer serializeTransferableArray(final Transferable<?>[] transferables, final Size arrayLengthSize) throws SerializationException { return serializeTransferableArray(transferables, 0, transferables.length, arrayLengthSize); } /** @param transferables will be serialized. None of the transferables can be null. * @param start index from which the transferables should be serialized. * @param count amount of transferables to serialize. * @param arrayLengthSize estimated amount of bytes needed to store length of the array. * @throws SerializationException is array length size is too small to store array's length or if unable to * serialize any of the transferables. * @return this (for chaining). */ public Serializer serializeTransferableArray(final Transferable<?>[] transferables, final int start, final int count, final Size arrayLengthSize) throws SerializationException { if (transferables == null) { serializeInt(Size.NULL_ARRAY_ID, arrayLengthSize); return this; } final int length = start + count; arrayLengthSize.validateArrayLengthToSerialize(length); serializeInt(length, arrayLengthSize); for (int index = start; index < length; index++) { serializeTransferable(transferables[index]); } return this; } /** @param transferables will be serialized using 4 bytes to store array length. Will use 1 extra byte for each * transferable to detect nullability. * @throws SerializationException if unable to serialize any of the transferables. * @return this (for chaining). */ public Serializer serializeTransferableArrayWithPossibleNulls(final Transferable<?>[] transferables) throws SerializationException { return serializeTransferableArrayWithPossibleNulls(transferables, Size.getDefaultArrayLengthSize()); } /** @param transferables will be serialized. Will use 1 extra byte for each transferable to detect nullability. * @param arrayLengthSize estimated amount of bytes needed to store length of the array. * @throws SerializationException is array length size is too small to store array's length or if unable to * serialize any of the transferables. * @return this (for chaining). */ public Serializer serializeTransferableArrayWithPossibleNulls(final Transferable<?>[] transferables, final Size arrayLengthSize) throws SerializationException { if (transferables == null) { serializeInt(Size.NULL_ARRAY_ID, arrayLengthSize); return this; } arrayLengthSize.validateArrayLengthToSerialize(transferables.length); serializeInt(transferables.length, arrayLengthSize); for (final Transferable<?> transferable : transferables) { if (transferable != null) { serializeByte(Byte.MAX_VALUE); serializeTransferable(transferable); } else { serializeInt(Size.NULL_ARRAY_ID, Size.BYTE); } } return this; } /** @param transferables will be serialized. Will use 1 extra byte for each transferable to detect nullability. * @param start index from which the transferables should be serialized. * @param count amount of transferables to serialize. * @param arrayLengthSize estimated amount of bytes needed to store length of the array. * @throws SerializationException is array length size is too small to store array's length or if unable to * serialize any of the transferables. * @return this (for chaining). */ public Serializer serializeTransferableArrayWithPossibleNulls(final Transferable<?>[] transferables, final int start, final int count, final Size arrayLengthSize) throws SerializationException { if (transferables == null) { serializeInt(Size.NULL_ARRAY_ID, arrayLengthSize); return this; } final int length = start + count; arrayLengthSize.validateArrayLengthToSerialize(length); serializeInt(length, arrayLengthSize); for (int index = start; index < length; index++) { final Transferable<?> transferable = transferables[index]; if (transferable != null) { serializeByte(Byte.MAX_VALUE); serializeTransferable(transferable); } else { serializeInt(Size.NULL_ARRAY_ID, Size.BYTE); } } return this; } /** Finishes serialization, returning the object as a byte array. * * @return serialized object as byte array. */ public byte[] serialize() { if (currentByteArrayIndex == 0) { return new byte[0]; } final byte[] extractedSerializedData = new byte[currentByteArrayIndex]; System.arraycopy(serializedData, 0, extractedSerializedData, 0, currentByteArrayIndex); return extractedSerializedData; } /** Finishes serialization, returning the object as a byte array. Contrary to {@link #serialize()}, this method * might return the internal serializer's byte array reference, which might get modified if the serializer is reset * and used to serialize another object. Safe to use if a new instance of Serializer is used for each serialization. * * @return serialized object as byte array. Might be the internal serializer's byte array. */ public byte[] serializeUnsafe() { if (currentByteArrayIndex == serializedData.length) { return serializedData; } return serialize(); } }