/*
* Licensed to Crate under one or more contributor license agreements.
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership. Crate licenses this file
* to you 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.
*
* However, if you have executed another commercial license agreement
* with Crate these terms will supersede the license and you may use the
* software solely pursuant to the terms of the relevant commercial
* agreement.
*/
package io.crate.protocols.postgres.types;
import com.google.common.primitives.Bytes;
import org.jboss.netty.buffer.ChannelBuffer;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.List;
class PGArray extends PGType {
private final PGType innerType;
static final PGArray CHAR_ARRAY = new PGArray(1002, CharType.INSTANCE);
static final PGArray INT2_ARRAY = new PGArray(1005, SmallIntType.INSTANCE);
static final PGArray INT4_ARRAY = new PGArray(1007, IntegerType.INSTANCE);
static final PGArray INT8_ARRAY = new PGArray(1016, BigIntType.INSTANCE);
static final PGArray FLOAT4_ARRAY = new PGArray(1021, RealType.INSTANCE);
static final PGArray FLOAT8_ARRAY = new PGArray(1022, DoubleType.INSTANCE);
static final PGArray BOOL_ARRAY = new PGArray(1000, BooleanType.INSTANCE);
static final PGArray TIMESTAMPZ_ARRAY = new PGArray(1185, TimestampType.INSTANCE);
static final PGArray VARCHAR_ARRAY = new PGArray(1015, VarCharType.INSTANCE);
static final PGArray JSON_ARRAY = new PGArray(199, JsonType.INSTANCE);
private static final byte[] NULL_BYTES = new byte[]{'N', 'U', 'L', 'L'};
private PGArray(int oid, PGType innerType) {
super(oid, -1, -1, "_" + innerType.typName());
this.innerType = innerType;
}
@Override
public int typElem() {
return innerType.oid();
}
@Override
public int writeAsBinary(ChannelBuffer buffer, @Nonnull Object value) {
int dimensions = getDimensions(value);
List<Integer> dimensionsList = new ArrayList<>();
buildDimensions((Object[]) value, dimensionsList, dimensions, 1);
int bytesWritten = 4 + 4 + 4;
int lenIndex = buffer.writerIndex();
buffer.writeInt(0);
buffer.writeInt(dimensions);
buffer.writeInt(1); // flags bit 0: 0=no-nulls, 1=has-nulls
buffer.writeInt(typElem());
for (Integer dim : dimensionsList) {
buffer.writeInt(dim); // upper bound
buffer.writeInt(dim); // lower bound
bytesWritten += 8;
}
int len = bytesWritten + writeArrayAsBinary(buffer, (Object[]) value, dimensionsList, 1);
buffer.setInt(lenIndex, len);
return len;
}
private int getDimensions(@Nonnull Object value) {
int dimensions = 0;
Object array = value;
do {
dimensions++;
Object[] arr = (Object[]) array;
if (arr.length == 0) {
break;
}
array = null;
for (Object o : arr) {
if (o == null) {
continue;
}
array = o;
}
} while (array != null && array.getClass().isArray());
return dimensions;
}
@Override
public Object readBinaryValue(ChannelBuffer buffer, int valueLength) {
int dimensions = buffer.readInt();
buffer.readInt(); // flags bit 0: 0=no-nulls, 1=has-nulls
buffer.readInt(); // element oid
if (dimensions == 0) {
return new Object[0];
}
int[] dims = new int[dimensions];
for (int d = 0; d < dimensions; ++d) {
dims[d] = buffer.readInt();
buffer.readInt(); // lowerBound ignored
}
Object[] array = new Object[dims[0]];
readArrayAsBinary(buffer, array, dims, 0);
return array;
}
@Override
byte[] encodeAsUTF8Text(@Nonnull Object array) {
boolean isJson = JsonType.OID == innerType.oid();
Object[] values = (Object[]) array;
List<Byte> encodedValues = new ArrayList<>();
encodedValues.add((byte) '{');
for (int i = 0; i < values.length; i++) {
Object o = values[i];
if (o instanceof Object[]) { // Nested Array -> recursive call
byte[] bytes = encodeAsUTF8Text(o);
for (byte b : bytes) {
encodedValues.add(b);
}
if (i == 0) {
encodedValues.add((byte) ',');
}
} else {
if (i > 0) {
encodedValues.add((byte) ',');
}
byte[] bytes;
if (o == null) {
bytes = NULL_BYTES;
for (byte aByte : bytes) {
encodedValues.add(aByte);
}
} else {
bytes = innerType.encodeAsUTF8Text(o);
encodedValues.add((byte) '"');
if (isJson) {
for (byte aByte : bytes) {
// Escape double quotes with backslash for json
if ((char) aByte == '"') {
encodedValues.add((byte) '\\');
}
encodedValues.add(aByte);
}
} else {
for (byte aByte : bytes) {
encodedValues.add(aByte);
}
}
encodedValues.add((byte) '"');
}
}
}
encodedValues.add((byte) '}');
return Bytes.toArray(encodedValues);
}
@Override
Object decodeUTF8Text(byte[] bytes) {
/*
* text representation:
*
* 1-dimension integer array:
* {"10",NULL,NULL,"20","30"}
* 2-dimension integer array:
* {{"10","20"},{"30",NULL,"40}}
*
* 1-dimension json array:
* {"{"x": 10}","{"y": 20}"}
* 2-dimension json array:
* {{"{"x": 10}","{"y": 20}"},{"{"x": 30}","{"y": 40}"}}
*/
List<Object> values = new ArrayList<>();
decodeUTF8Text(bytes, 0, bytes.length - 1, values);
return values.toArray();
}
private int decodeUTF8Text(byte[] bytes, int startIdx, int endIdx, List<Object> objects) {
int valIdx = startIdx;
boolean jsonObject = false;
for (int i = startIdx; i <= endIdx; i++) {
byte aByte = bytes[i];
switch (aByte) {
case '{':
if (i == 0 || bytes[i - 1] != '"') {
if (i > 0) {
// n-dimensions array -> call recursively
List<Object> nestedObjects = new ArrayList<>();
i = decodeUTF8Text(bytes, i + 1, endIdx, nestedObjects);
valIdx = i;
objects.add(nestedObjects.toArray());
} else {
// 1-dimension array -> call recursively
i = decodeUTF8Text(bytes, i + 1, endIdx, objects);
}
} else {
// Start of JSON object
valIdx = i - 1;
jsonObject = true;
}
break;
case ',':
if (!jsonObject) {
addObject(bytes, valIdx, i - 1, objects);
valIdx = i + 1;
}
break;
case '}':
if (i == endIdx || bytes[i + 1] != '"') {
addObject(bytes, valIdx, i - 1, objects);
return i;
} else {
//End of JSON object
addObject(bytes, valIdx, i + 1, objects);
jsonObject = false;
i++;
valIdx = i;
if (bytes[i] == '}') { // end of array
return i + 2;
}
}
}
}
return endIdx;
}
// Decode individual inner object
private void addObject(byte[] bytes, int startIdx, int endIdx, List<Object> objects) {
if (endIdx > startIdx) {
byte firstValueByte = bytes[startIdx];
if (firstValueByte == '"') {
List<Byte> innerBytes = new ArrayList<>(endIdx - (startIdx + 1));
for (int i = startIdx + 1; i < endIdx; i++) {
if (i < (endIdx - 1) && (char) bytes[i] == '\\' &&
((char) bytes[i + 1] == '\\' || (char) bytes[i + 1] == '\"')) {
i++;
}
innerBytes.add(bytes[i]);
}
objects.add(innerType.decodeUTF8Text(Bytes.toArray(innerBytes)));
} else if (firstValueByte == 'N') {
objects.add(null);
}
}
}
private int buildDimensions(Object[] array, List<Integer> dimensionsList, int maxDimensions, int currentDimension) {
if (array == null) {
return 1;
}
// While elements of array are also arrays
if (currentDimension < maxDimensions) {
int max = 0;
for (Object o : array) {
max = Math.max(max, buildDimensions((Object[]) o, dimensionsList, maxDimensions, currentDimension + 1));
}
if (currentDimension == maxDimensions - 1) {
dimensionsList.add(max);
} else {
Integer current = dimensionsList.get(0);
dimensionsList.set(0, Math.max(current, max));
}
}
// Add the dimensions of 1st dimension
if (currentDimension == 1) {
dimensionsList.add(0, array.length);
}
return array.length;
}
private int writeArrayAsBinary(ChannelBuffer buffer, Object[] array, List<Integer> dimensionsList, int currentDimension) {
int bytesWritten = 0;
if (array == null) {
for (int i = 0; i < dimensionsList.get(currentDimension - 1); i++) {
buffer.writeInt(-1);
bytesWritten += 4;
}
return bytesWritten;
}
// 2nd to last level
if (currentDimension == dimensionsList.size()) {
int i = 0;
for (Object o : array) {
if (o == null) {
buffer.writeInt(-1);
bytesWritten += 4;
} else {
bytesWritten += innerType.writeAsBinary(buffer, o);
}
i++;
}
// Fill in with -1 for up to max dimensions
for (; i < dimensionsList.get(currentDimension - 1); i++) {
buffer.writeInt(-1);
bytesWritten += 4;
}
} else {
for (Object o : array) {
bytesWritten += writeArrayAsBinary(buffer, (Object[]) o, dimensionsList, currentDimension + 1);
}
}
return bytesWritten;
}
private void readArrayAsBinary(ChannelBuffer buffer,
final Object[] array,
final int[] dims,
final int thisDimension) {
if (thisDimension == dims.length - 1) {
for (int i = 0; i < dims[thisDimension]; ++i) {
int len = buffer.readInt();
if (len == -1) {
continue;
}
array[i] = innerType.readBinaryValue(buffer, len);
}
} else {
for (int i = 0; i < dims[thisDimension]; ++i) {
array[i] = new Object[dims[thisDimension + 1]];
readArrayAsBinary(buffer, (Object[]) array[i], dims, thisDimension + 1);
}
}
}
}