org.postgresql.jdbc.ArrayEncoding Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jdbc-yugabytedb Show documentation
Show all versions of jdbc-yugabytedb Show documentation
Java JDBC 4.2 (JRE 8+) driver for YugaByte SQL database
/*
* Copyright (c) 2020, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/
package org.postgresql.jdbc;
import org.postgresql.core.BaseConnection;
import org.postgresql.core.Encoding;
import org.postgresql.core.Oid;
import org.postgresql.util.ByteConverter;
import org.postgresql.util.GT;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;
import org.checkerframework.checker.index.qual.Positive;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.HashMap;
import java.util.Map;
/**
* Utility for using arrays in requests.
*
*
* Binary format:
*
* - 4 bytes with number of dimensions
* - 4 bytes, boolean indicating nulls present or not
* - 4 bytes type oid
* - 8 bytes describing the length of each dimension (repeated for each dimension)
*
* - 4 bytes for length
* - 4 bytes for lower bound on length to check for overflow (it appears this value can always be 0)
*
* - data in depth first element order corresponding number and length of dimensions
*
* - 4 bytes describing length of element, {@code 0xFFFFFFFF} ({@code -1}) means {@code null}
* - binary representation of element (iff not {@code null}).
*
*
*
*
* @author Brett Okken
*/
final class ArrayEncoding {
interface ArrayEncoder {
/**
* The default array type oid supported by this instance.
*
* @return The default array type oid supported by this instance.
*/
int getDefaultArrayTypeOid();
/**
* Creates {@code String} representation of the array.
*
* @param delim
* The character to use to delimit between elements.
* @param array
* The array to represent as a {@code String}.
* @return {@code String} representation of the array.
*/
String toArrayString(char delim, A array);
/**
* Indicates if an array can be encoded in binary form to array oid.
*
* @param oid
* The array oid to see check for binary support.
* @return Indication of whether
* {@link #toBinaryRepresentation(BaseConnection, Object, int)} is
* supported for oid.
*/
boolean supportBinaryRepresentation(int oid);
/**
* Creates binary representation of the array.
*
* @param connection
* The connection the binary representation will be used on. Attributes
* from the connection might impact how values are translated to
* binary.
* @param array
* The array to binary encode. Must not be {@code null}, but may
* contain {@code null} elements.
* @param oid
* The array type oid to use. Calls to
* {@link #supportBinaryRepresentation(int)} must have returned
* {@code true}.
* @return The binary representation of array.
* @throws SQLFeatureNotSupportedException
* If {@link #supportBinaryRepresentation(int)} is false for
* oid.
*/
byte[] toBinaryRepresentation(BaseConnection connection, A array, int oid)
throws SQLException, SQLFeatureNotSupportedException;
/**
* Append {@code String} representation of array to sb.
*
* @param sb
* The {@link StringBuilder} to append to.
* @param delim
* The delimiter between elements.
* @param array
* The array to represent. Will not be {@code null}, but may contain
* {@code null} elements.
*/
void appendArray(StringBuilder sb, char delim, A array);
}
/**
* Base class to implement {@link ArrayEncoding.ArrayEncoder} and provide
* multi-dimensional support.
*
* @param
* Base array type supported.
*/
private abstract static class AbstractArrayEncoder
implements ArrayEncoder {
private final int oid;
final int arrayOid;
/**
*
* @param oid
* The default/primary base oid type.
* @param arrayOid
* The default/primary array oid type.
*/
AbstractArrayEncoder(int oid, int arrayOid) {
this.oid = oid;
this.arrayOid = arrayOid;
}
/**
*
* @param arrayOid
* The array oid to get base oid type for.
* @return The base oid type for the given array oid type given to
* {@link #toBinaryRepresentation(BaseConnection, Object, int)}.
*/
int getTypeOID(@SuppressWarnings("unused") int arrayOid) {
return oid;
}
/**
* By default returns the arrayOid this instance was instantiated with.
*/
@Override
public int getDefaultArrayTypeOid() {
return arrayOid;
}
/**
* Counts the number of {@code null} elements in array.
*
* @param array
* The array to count {@code null} elements in.
* @return The number of {@code null} elements in array.
*/
int countNulls(A array) {
int nulls = 0;
final int arrayLength = Array.getLength(array);
for (int i = 0; i < arrayLength; ++i) {
if (Array.get(array, i) == null) {
++nulls;
}
}
return nulls;
}
/**
* Creates {@code byte[]} of just the raw data (no metadata).
*
* @param connection
* The connection the binary representation will be used on.
* @param array
* The array to create binary representation of. Will not be
* {@code null}, but may contain {@code null} elements.
* @return {@code byte[]} of just the raw data (no metadata).
* @throws SQLFeatureNotSupportedException
* If {@link #supportBinaryRepresentation(int)} is false for
* oid.
*/
abstract byte[] toSingleDimensionBinaryRepresentation(BaseConnection connection, A array)
throws SQLException, SQLFeatureNotSupportedException;
/**
* {@inheritDoc}
*/
@Override
public String toArrayString(char delim, A array) {
final StringBuilder sb = new StringBuilder(1024);
appendArray(sb, delim, array);
return sb.toString();
}
/**
* By default returns {@code true} if oid matches the arrayOid
* this instance was instantiated with.
*/
@Override
public boolean supportBinaryRepresentation(int oid) {
return oid == arrayOid;
}
}
/**
* Base class to provide support for {@code Number} based arrays.
*
* @param
* The base type of array.
*/
private abstract static class NumberArrayEncoder extends AbstractArrayEncoder {
private final int fieldSize;
/**
*
* @param fieldSize
* The fixed size to represent each value in binary.
* @param oid
* The base type oid.
* @param arrayOid
* The array type oid.
*/
NumberArrayEncoder(int fieldSize, int oid, int arrayOid) {
super(oid, arrayOid);
this.fieldSize = fieldSize;
}
/**
* {@inheritDoc}
*/
@Override
final int countNulls(N[] array) {
int count = 0;
for (int i = 0; i < array.length; ++i) {
if (array[i] == null) {
++count;
}
}
return count;
}
/**
* {@inheritDoc}
*/
@Override
public final byte[] toBinaryRepresentation(BaseConnection connection, N[] array, int oid)
throws SQLException, SQLFeatureNotSupportedException {
assert oid == this.arrayOid;
final int nullCount = countNulls(array);
final byte[] bytes = writeBytes(array, nullCount, 20);
// 1 dimension
ByteConverter.int4(bytes, 0, 1);
// no null
ByteConverter.int4(bytes, 4, nullCount == 0 ? 0 : 1);
// oid
ByteConverter.int4(bytes, 8, getTypeOID(oid));
// length
ByteConverter.int4(bytes, 12, array.length);
// postgresql uses 1 base by default
ByteConverter.int4(bytes, 16, 1);
return bytes;
}
/**
* {@inheritDoc}
*/
@Override
final byte[] toSingleDimensionBinaryRepresentation(BaseConnection connection, N[] array)
throws SQLException, SQLFeatureNotSupportedException {
final int nullCount = countNulls(array);
return writeBytes(array, nullCount, 0);
}
private byte[] writeBytes(final N[] array, final int nullCount, final int offset) {
final int length = offset + (4 * array.length) + (fieldSize * (array.length - nullCount));
final byte[] bytes = new byte[length];
int idx = offset;
for (int i = 0; i < array.length; ++i) {
if (array[i] == null) {
ByteConverter.int4(bytes, idx, -1);
idx += 4;
} else {
ByteConverter.int4(bytes, idx, fieldSize);
idx += 4;
write(array[i], bytes, idx);
idx += fieldSize;
}
}
return bytes;
}
/**
* Write single value (number) to bytes beginning at
* offset.
*
* @param number
* The value to write to bytes. This will never be {@code null}.
* @param bytes
* The {@code byte[]} to write to.
* @param offset
* The offset into bytes to write the number value.
*/
protected abstract void write(N number, byte[] bytes, int offset);
/**
* {@inheritDoc}
*/
@Override
public final void appendArray(StringBuilder sb, char delim, N[] array) {
sb.append('{');
for (int i = 0; i < array.length; ++i) {
if (i != 0) {
sb.append(delim);
}
if (array[i] == null) {
sb.append('N').append('U').append('L').append('L');
} else {
sb.append('"');
sb.append(array[i].toString());
sb.append('"');
}
}
sb.append('}');
}
}
/**
* Base support for primitive arrays.
*
* @param
* The primitive array to support.
*/
private abstract static class FixedSizePrimitiveArrayEncoder
extends AbstractArrayEncoder {
private final int fieldSize;
FixedSizePrimitiveArrayEncoder(int fieldSize, int oid, int arrayOid) {
super(oid, arrayOid);
this.fieldSize = fieldSize;
}
/**
* {@inheritDoc}
*
*
* Always returns {@code 0}.
*
*/
@Override
final int countNulls(A array) {
return 0;
}
/**
* {@inheritDoc}
*/
@Override
public final byte[] toBinaryRepresentation(BaseConnection connection, A array, int oid)
throws SQLException, SQLFeatureNotSupportedException {
assert oid == arrayOid;
final int arrayLength = Array.getLength(array);
final int length = 20 + ((fieldSize + 4) * arrayLength);
final byte[] bytes = new byte[length];
// 1 dimension
ByteConverter.int4(bytes, 0, 1);
// no null
ByteConverter.int4(bytes, 4, 0);
// oid
ByteConverter.int4(bytes, 8, getTypeOID(oid));
// length
ByteConverter.int4(bytes, 12, arrayLength);
// postgresql uses 1 base by default
ByteConverter.int4(bytes, 16, 1);
write(array, bytes, 20);
return bytes;
}
/**
* {@inheritDoc}
*/
@Override
final byte[] toSingleDimensionBinaryRepresentation(BaseConnection connection, A array)
throws SQLException, SQLFeatureNotSupportedException {
final int length = ((fieldSize + 4) * Array.getLength(array));
final byte[] bytes = new byte[length];
write(array, bytes, 0);
return bytes;
}
/**
* Write the entire contents of array to bytes starting at
* offset without metadata describing type or length.
*
* @param array
* The array to write.
* @param bytes
* The {@code byte[]} to write to.
* @param offset
* The offset into bytes to start writing.
*/
protected abstract void write(A array, byte[] bytes, int offset);
}
private static final AbstractArrayEncoder LONG_ARRAY = new FixedSizePrimitiveArrayEncoder(8, Oid.INT8,
Oid.INT8_ARRAY) {
/**
* {@inheritDoc}
*/
@Override
public void appendArray(StringBuilder sb, char delim, long[] array) {
sb.append('{');
for (int i = 0; i < array.length; ++i) {
if (i > 0) {
sb.append(delim);
}
sb.append(array[i]);
}
sb.append('}');
}
/**
* {@inheritDoc}
*/
@Override
protected void write(long[] array, byte[] bytes, int offset) {
int idx = offset;
for (int i = 0; i < array.length; ++i) {
bytes[idx + 3] = 8;
ByteConverter.int8(bytes, idx + 4, array[i]);
idx += 12;
}
}
};
private static final AbstractArrayEncoder LONG_OBJ_ARRAY = new NumberArrayEncoder(8, Oid.INT8,
Oid.INT8_ARRAY) {
@Override
protected void write(Long number, byte[] bytes, int offset) {
ByteConverter.int8(bytes, offset, number.longValue());
}
};
private static final AbstractArrayEncoder INT_ARRAY = new FixedSizePrimitiveArrayEncoder(4, Oid.INT4,
Oid.INT4_ARRAY) {
/**
* {@inheritDoc}
*/
@Override
public void appendArray(StringBuilder sb, char delim, int[] array) {
sb.append('{');
for (int i = 0; i < array.length; ++i) {
if (i > 0) {
sb.append(delim);
}
sb.append(array[i]);
}
sb.append('}');
}
/**
* {@inheritDoc}
*/
@Override
protected void write(int[] array, byte[] bytes, int offset) {
int idx = offset;
for (int i = 0; i < array.length; ++i) {
bytes[idx + 3] = 4;
ByteConverter.int4(bytes, idx + 4, array[i]);
idx += 8;
}
}
};
private static final AbstractArrayEncoder INT_OBJ_ARRAY = new NumberArrayEncoder(4, Oid.INT4,
Oid.INT4_ARRAY) {
@Override
protected void write(Integer number, byte[] bytes, int offset) {
ByteConverter.int4(bytes, offset, number.intValue());
}
};
private static final AbstractArrayEncoder SHORT_ARRAY = new FixedSizePrimitiveArrayEncoder(2,
Oid.INT2, Oid.INT2_ARRAY) {
/**
* {@inheritDoc}
*/
@Override
public void appendArray(StringBuilder sb, char delim, short[] array) {
sb.append('{');
for (int i = 0; i < array.length; ++i) {
if (i > 0) {
sb.append(delim);
}
sb.append(array[i]);
}
sb.append('}');
}
/**
* {@inheritDoc}
*/
@Override
protected void write(short[] array, byte[] bytes, int offset) {
int idx = offset;
for (int i = 0; i < array.length; ++i) {
bytes[idx + 3] = 2;
ByteConverter.int2(bytes, idx + 4, array[i]);
idx += 6;
}
}
};
private static final AbstractArrayEncoder SHORT_OBJ_ARRAY = new NumberArrayEncoder(2, Oid.INT2,
Oid.INT2_ARRAY) {
/**
* {@inheritDoc}
*/
@Override
protected void write(Short number, byte[] bytes, int offset) {
ByteConverter.int2(bytes, offset, number.shortValue());
}
};
private static final AbstractArrayEncoder DOUBLE_ARRAY = new FixedSizePrimitiveArrayEncoder(8,
Oid.FLOAT8, Oid.FLOAT8_ARRAY) {
/**
* {@inheritDoc}
*/
@Override
public void appendArray(StringBuilder sb, char delim, double[] array) {
sb.append('{');
for (int i = 0; i < array.length; ++i) {
if (i > 0) {
sb.append(delim);
}
// use quotes to account for any issues with scientific notation
sb.append('"');
sb.append(array[i]);
sb.append('"');
}
sb.append('}');
}
/**
* {@inheritDoc}
*/
@Override
protected void write(double[] array, byte[] bytes, int offset) {
int idx = offset;
for (int i = 0; i < array.length; ++i) {
bytes[idx + 3] = 8;
ByteConverter.float8(bytes, idx + 4, array[i]);
idx += 12;
}
}
};
private static final AbstractArrayEncoder DOUBLE_OBJ_ARRAY = new NumberArrayEncoder(8, Oid.FLOAT8,
Oid.FLOAT8_ARRAY) {
/**
* {@inheritDoc}
*/
@Override
protected void write(Double number, byte[] bytes, int offset) {
ByteConverter.float8(bytes, offset, number.doubleValue());
}
};
private static final AbstractArrayEncoder FLOAT_ARRAY = new FixedSizePrimitiveArrayEncoder(4,
Oid.FLOAT4, Oid.FLOAT4_ARRAY) {
/**
* {@inheritDoc}
*/
@Override
public void appendArray(StringBuilder sb, char delim, float[] array) {
sb.append('{');
for (int i = 0; i < array.length; ++i) {
if (i > 0) {
sb.append(delim);
}
// use quotes to account for any issues with scientific notation
sb.append('"');
sb.append(array[i]);
sb.append('"');
}
sb.append('}');
}
/**
* {@inheritDoc}
*/
@Override
protected void write(float[] array, byte[] bytes, int offset) {
int idx = offset;
for (int i = 0; i < array.length; ++i) {
bytes[idx + 3] = 4;
ByteConverter.float4(bytes, idx + 4, array[i]);
idx += 8;
}
}
};
private static final AbstractArrayEncoder FLOAT_OBJ_ARRAY = new NumberArrayEncoder(4, Oid.FLOAT4,
Oid.FLOAT4_ARRAY) {
/**
* {@inheritDoc}
*/
@Override
protected void write(Float number, byte[] bytes, int offset) {
ByteConverter.float4(bytes, offset, number.floatValue());
}
};
private static final AbstractArrayEncoder BOOLEAN_ARRAY = new FixedSizePrimitiveArrayEncoder(1,
Oid.BOOL, Oid.BOOL_ARRAY) {
/**
* {@inheritDoc}
*/
@Override
public void appendArray(StringBuilder sb, char delim, boolean[] array) {
sb.append('{');
for (int i = 0; i < array.length; ++i) {
if (i > 0) {
sb.append(delim);
}
sb.append(array[i] ? '1' : '0');
}
sb.append('}');
}
/**
* {@inheritDoc}
*/
@Override
protected void write(boolean[] array, byte[] bytes, int offset) {
int idx = offset;
for (int i = 0; i < array.length; ++i) {
bytes[idx + 3] = 1;
ByteConverter.bool(bytes, idx + 4, array[i]);
idx += 5;
}
}
};
private static final AbstractArrayEncoder BOOLEAN_OBJ_ARRAY = new AbstractArrayEncoder(Oid.BOOL,
Oid.BOOL_ARRAY) {
/**
* {@inheritDoc}
*/
@Override
public byte[] toBinaryRepresentation(BaseConnection connection, Boolean[] array, int oid)
throws SQLException, SQLFeatureNotSupportedException {
assert oid == arrayOid;
final int nullCount = countNulls(array);
final byte[] bytes = writeBytes(array, nullCount, 20);
// 1 dimension
ByteConverter.int4(bytes, 0, 1);
// no null
ByteConverter.int4(bytes, 4, nullCount == 0 ? 0 : 1);
// oid
ByteConverter.int4(bytes, 8, getTypeOID(oid));
// length
ByteConverter.int4(bytes, 12, array.length);
// postgresql uses 1 base by default
ByteConverter.int4(bytes, 16, 1);
return bytes;
}
private byte[] writeBytes(final Boolean[] array, final int nullCount, final int offset) {
final int length = offset + (4 * array.length) + (array.length - nullCount);
final byte[] bytes = new byte[length];
int idx = offset;
for (int i = 0; i < array.length; ++i) {
if (array[i] == null) {
ByteConverter.int4(bytes, idx, -1);
idx += 4;
} else {
ByteConverter.int4(bytes, idx, 1);
idx += 4;
write(array[i], bytes, idx);
++idx;
}
}
return bytes;
}
private void write(Boolean bool, byte[] bytes, int idx) {
ByteConverter.bool(bytes, idx, bool.booleanValue());
}
/**
* {@inheritDoc}
*/
@Override
byte[] toSingleDimensionBinaryRepresentation(BaseConnection connection, Boolean[] array)
throws SQLException, SQLFeatureNotSupportedException {
final int nullCount = countNulls(array);
return writeBytes(array, nullCount, 0);
}
/**
* {@inheritDoc}
*/
@Override
public void appendArray(StringBuilder sb, char delim, Boolean[] array) {
sb.append('{');
for (int i = 0; i < array.length; ++i) {
if (i != 0) {
sb.append(delim);
}
if (array[i] == null) {
sb.append('N').append('U').append('L').append('L');
} else {
sb.append(array[i].booleanValue() ? '1' : '0');
}
}
sb.append('}');
}
};
private static final AbstractArrayEncoder STRING_ARRAY = new AbstractArrayEncoder(Oid.VARCHAR,
Oid.VARCHAR_ARRAY) {
/**
* {@inheritDoc}
*/
@Override
int countNulls(String[] array) {
int count = 0;
for (int i = 0; i < array.length; ++i) {
if (array[i] == null) {
++count;
}
}
return count;
}
/**
* {@inheritDoc}
*/
@Override
public boolean supportBinaryRepresentation(int oid) {
return oid == Oid.VARCHAR_ARRAY || oid == Oid.TEXT_ARRAY;
}
/**
* {@inheritDoc}
*/
@Override
int getTypeOID(int arrayOid) {
if (arrayOid == Oid.VARCHAR_ARRAY) {
return Oid.VARCHAR;
}
if (arrayOid == Oid.TEXT_ARRAY) {
return Oid.TEXT;
}
// this should not be possible based on supportBinaryRepresentation returning
// false for all other types
throw new IllegalStateException("Invalid array oid: " + arrayOid);
}
/**
* {@inheritDoc}
*/
@Override
public void appendArray(StringBuilder sb, char delim, String[] array) {
sb.append('{');
for (int i = 0; i < array.length; ++i) {
if (i > 0) {
sb.append(delim);
}
if (array[i] == null) {
sb.append('N').append('U').append('L').append('L');
} else {
PgArray.escapeArrayElement(sb, array[i]);
}
}
sb.append('}');
}
/**
* {@inheritDoc}
*/
@Override
public byte[] toBinaryRepresentation(BaseConnection connection, String[] array, int oid) throws SQLException {
final ByteArrayOutputStream baos = new ByteArrayOutputStream(Math.min(1024, (array.length * 32) + 20));
assert supportBinaryRepresentation(oid);
final byte[] buffer = new byte[4];
try {
// 1 dimension
ByteConverter.int4(buffer, 0, 1);
baos.write(buffer);
// null
ByteConverter.int4(buffer, 0, countNulls(array) > 0 ? 1 : 0);
baos.write(buffer);
// oid
ByteConverter.int4(buffer, 0, getTypeOID(oid));
baos.write(buffer);
// length
ByteConverter.int4(buffer, 0, array.length);
baos.write(buffer);
// postgresql uses 1 base by default
ByteConverter.int4(buffer, 0, 1);
baos.write(buffer);
final Encoding encoding = connection.getEncoding();
for (int i = 0; i < array.length; ++i) {
final String string = array[i];
if (string != null) {
final byte[] encoded;
try {
encoded = encoding.encode(string);
} catch (IOException e) {
throw new PSQLException(GT.tr("Unable to translate data into the desired encoding."),
PSQLState.DATA_ERROR, e);
}
ByteConverter.int4(buffer, 0, encoded.length);
baos.write(buffer);
baos.write(encoded);
} else {
ByteConverter.int4(buffer, 0, -1);
baos.write(buffer);
}
}
return baos.toByteArray();
} catch (IOException e) {
// this IO exception is from writing to baos, which will never throw an
// IOException
throw new java.lang.AssertionError(e);
}
}
/**
* {@inheritDoc}
*/
@Override
byte[] toSingleDimensionBinaryRepresentation(BaseConnection connection, String[] array)
throws SQLException, SQLFeatureNotSupportedException {
try {
final ByteArrayOutputStream baos = new ByteArrayOutputStream(Math.min(1024, (array.length * 32) + 20));
final byte[] buffer = new byte[4];
final Encoding encoding = connection.getEncoding();
for (int i = 0; i < array.length; ++i) {
final String string = array[i];
if (string != null) {
final byte[] encoded;
try {
encoded = encoding.encode(string);
} catch (IOException e) {
throw new PSQLException(GT.tr("Unable to translate data into the desired encoding."),
PSQLState.DATA_ERROR, e);
}
ByteConverter.int4(buffer, 0, encoded.length);
baos.write(buffer);
baos.write(encoded);
} else {
ByteConverter.int4(buffer, 0, -1);
baos.write(buffer);
}
}
return baos.toByteArray();
} catch (IOException e) {
// this IO exception is from writing to baos, which will never throw an
// IOException
throw new java.lang.AssertionError(e);
}
}
};
private static final AbstractArrayEncoder BYTEA_ARRAY = new AbstractArrayEncoder(Oid.BYTEA,
Oid.BYTEA_ARRAY) {
/**
* The possible characters to use for representing hex binary data.
*/
private final char[] hexDigits = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
'e', 'f' };
/**
* {@inheritDoc}
*/
@Override
public byte[] toBinaryRepresentation(BaseConnection connection, byte[][] array, int oid)
throws SQLException, SQLFeatureNotSupportedException {
assert oid == arrayOid;
int length = 20;
for (int i = 0; i < array.length; ++i) {
length += 4;
if (array[i] != null) {
length += array[i].length;
}
}
final byte[] bytes = new byte[length];
// 1 dimension
ByteConverter.int4(bytes, 0, 1);
// no null
ByteConverter.int4(bytes, 4, 0);
// oid
ByteConverter.int4(bytes, 8, getTypeOID(oid));
// length
ByteConverter.int4(bytes, 12, array.length);
// postgresql uses 1 base by default
ByteConverter.int4(bytes, 16, 1);
write(array, bytes, 20);
return bytes;
}
/**
* {@inheritDoc}
*/
@Override
byte[] toSingleDimensionBinaryRepresentation(BaseConnection connection, byte[][] array)
throws SQLException, SQLFeatureNotSupportedException {
int length = 0;
for (int i = 0; i < array.length; ++i) {
length += 4;
if (array[i] != null) {
length += array[i].length;
}
}
final byte[] bytes = new byte[length];
write(array, bytes, 0);
return bytes;
}
/**
* {@inheritDoc}
*/
@Override
int countNulls(byte[][] array) {
int nulls = 0;
for (int i = 0; i < array.length; ++i) {
if (array[i] == null) {
++nulls;
}
}
return nulls;
}
private void write(byte[][] array, byte[] bytes, int offset) {
int idx = offset;
for (int i = 0; i < array.length; ++i) {
if (array[i] != null) {
ByteConverter.int4(bytes, idx, array[i].length);
idx += 4;
System.arraycopy(array[i], 0, bytes, idx, array[i].length);
idx += array[i].length;
} else {
ByteConverter.int4(bytes, idx, -1);
idx += 4;
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void appendArray(StringBuilder sb, char delim, byte[][] array) {
sb.append('{');
for (int i = 0; i < array.length; ++i) {
if (i > 0) {
sb.append(delim);
}
if (array[i] != null) {
sb.append("\"\\\\x");
for (int j = 0; j < array[i].length; ++j) {
byte b = array[i][j];
// get the value for the left 4 bits (drop sign)
sb.append(hexDigits[(b & 0xF0) >>> 4]);
// get the value for the right 4 bits
sb.append(hexDigits[b & 0x0F]);
}
sb.append('"');
} else {
sb.append("NULL");
}
}
sb.append('}');
}
};
private static final AbstractArrayEncoder