/*
* Licensed 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.
*/
package com.facebook.presto.type;
import com.facebook.presto.spi.ConnectorSession;
import com.facebook.presto.spi.PrestoException;
import com.facebook.presto.spi.StandardErrorCode;
import com.facebook.presto.spi.block.BlockBuilder;
import com.facebook.presto.spi.block.BlockBuilderStatus;
import com.facebook.presto.spi.type.DecimalType;
import com.facebook.presto.spi.type.Decimals;
import com.facebook.presto.spi.type.FixedWidthType;
import com.facebook.presto.spi.type.SqlDecimal;
import com.facebook.presto.spi.type.StandardTypes;
import com.facebook.presto.spi.type.Type;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Throwables;
import io.airlift.slice.Slice;
import io.airlift.slice.Slices;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static com.facebook.presto.spi.type.RealType.REAL;
import static com.fasterxml.jackson.core.JsonFactory.Feature.CANONICALIZE_FIELD_NAMES;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static java.lang.Float.floatToRawIntBits;
import static java.util.Objects.requireNonNull;
public final class TypeJsonUtils
{
private static final JsonFactory JSON_FACTORY = new JsonFactory().disable(CANONICALIZE_FIELD_NAMES);
// This object mapper is constructed without .configure(ORDER_MAP_ENTRIES_BY_KEYS, true) because
// `OBJECT_MAPPER.writeValueAsString(parser.readValueAsTree());` preserves input order.
// Be aware. Using it arbitrarily can produce invalid json (ordered by key is required in presto).
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(JSON_FACTORY);
private TypeJsonUtils() {}
public static Object stackRepresentationToObject(ConnectorSession session, Slice value, Type type)
{
if (value == null) {
return null;
}
try (JsonParser jsonParser = JSON_FACTORY.createParser((InputStream) value.getInput())) {
jsonParser.nextToken();
return stackRepresentationToObjectHelper(session, jsonParser, type);
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
private static Object stackRepresentationToObjectHelper(ConnectorSession session, JsonParser parser, Type type)
throws IOException
{
// checking whether type is JsonType needs to go before null check because
// cast('[null]', array(json)) should be casted to a single item array containing a json document "null" instead of sql null.
if (type instanceof JsonType) {
return OBJECT_MAPPER.writeValueAsString(parser.readValueAsTree());
}
if (parser.getCurrentToken() == JsonToken.VALUE_NULL) {
return null;
}
if (type instanceof ArrayType) {
List<Object> list = new ArrayList<>();
checkState(parser.getCurrentToken() == JsonToken.START_ARRAY, "Expected a json array");
while (parser.nextToken() != JsonToken.END_ARRAY) {
list.add(stackRepresentationToObjectHelper(session, parser, ((ArrayType) type).getElementType()));
}
return Collections.unmodifiableList(list);
}
if (type instanceof MapType) {
Map<Object, Object> map = new LinkedHashMap<>();
checkState(parser.getCurrentToken() == JsonToken.START_OBJECT, "Expected a json object");
while (parser.nextValue() != JsonToken.END_OBJECT) {
Object key = mapKeyToObject(session, parser.getCurrentName(), ((MapType) type).getKeyType());
Object value = stackRepresentationToObjectHelper(session, parser, ((MapType) type).getValueType());
map.put(key, value);
}
return Collections.unmodifiableMap(map);
}
if (type instanceof RowType) {
List<Object> list = new ArrayList<>();
checkState(parser.getCurrentToken() == JsonToken.START_ARRAY, "Expected a json array");
int field = 0;
RowType rowType = (RowType) type;
while (parser.nextValue() != JsonToken.END_ARRAY) {
checkArgument(field < rowType.getFields().size(), "Unexpected field for type %s", type);
Object value = stackRepresentationToObjectHelper(session, parser, rowType.getFields().get(field).getType());
list.add(value);
field++;
}
checkArgument(field == rowType.getFields().size(), "Expected %s fields for type %s", rowType.getFields().size(), type);
return Collections.unmodifiableList(list);
}
Slice sliceValue = null;
if (type.getJavaType() == Slice.class) {
sliceValue = Slices.utf8Slice(parser.getValueAsString());
}
BlockBuilder blockBuilder;
if (type instanceof FixedWidthType) {
blockBuilder = type.createBlockBuilder(new BlockBuilderStatus(), 1);
}
else {
blockBuilder = type.createBlockBuilder(new BlockBuilderStatus(), 1, requireNonNull(sliceValue, "sliceValue is null").length());
}
if (type instanceof DecimalType) {
return getSqlDecimal((DecimalType) type, parser.getDecimalValue());
}
else if (type.getJavaType() == boolean.class) {
type.writeBoolean(blockBuilder, parser.getBooleanValue());
}
else if (type.getJavaType() == long.class) {
if (type.equals(REAL)) {
type.writeLong(blockBuilder, floatToRawIntBits(parser.getFloatValue()));
}
else {
type.writeLong(blockBuilder, parser.getLongValue());
}
}
else if (type.getJavaType() == double.class) {
type.writeDouble(blockBuilder, getDoubleValue(parser));
}
else if (type.getJavaType() == Slice.class) {
type.writeSlice(blockBuilder, requireNonNull(sliceValue, "sliceValue is null"));
}
return type.getObjectValue(session, blockBuilder.build(), 0);
}
private static SqlDecimal getSqlDecimal(DecimalType decimalType, BigDecimal decimalValue)
{
BigInteger unscaledValue = decimalValue.setScale(decimalType.getScale(), RoundingMode.HALF_UP).unscaledValue();
if (Decimals.overflows(unscaledValue, decimalType.getPrecision())) {
throw new PrestoException(StandardErrorCode.INVALID_FUNCTION_ARGUMENT, String.format("DECIMAL with unscaled value %s exceeds precision %s", unscaledValue, decimalType.getPrecision()));
}
return new SqlDecimal(unscaledValue,
decimalType.getPrecision(),
decimalType.getScale());
}
private static Object mapKeyToObject(ConnectorSession session, String jsonKey, Type type)
{
BlockBuilder blockBuilder;
if (type instanceof FixedWidthType) {
blockBuilder = type.createBlockBuilder(new BlockBuilderStatus(), 1);
}
else {
blockBuilder = type.createBlockBuilder(new BlockBuilderStatus(), 1, jsonKey.length());
}
if (type instanceof DecimalType) {
DecimalType decimalType = (DecimalType) type;
return getSqlDecimal(decimalType, new BigDecimal(jsonKey));
}
else if (type.getJavaType() == boolean.class) {
type.writeBoolean(blockBuilder, Boolean.parseBoolean(jsonKey));
}
else if (type.getJavaType() == long.class) {
type.writeLong(blockBuilder, Long.parseLong(jsonKey));
}
else if (type.getJavaType() == double.class) {
type.writeDouble(blockBuilder, Double.parseDouble(jsonKey));
}
else if (type.getJavaType() == Slice.class) {
type.writeSlice(blockBuilder, Slices.utf8Slice(jsonKey));
}
return type.getObjectValue(session, blockBuilder.build(), 0);
}
private static double getDoubleValue(JsonParser parser)
throws IOException
{
double value;
try {
value = parser.getDoubleValue();
}
catch (JsonParseException e) {
//handle non-numeric numbers (inf/nan)
value = Double.parseDouble(parser.getValueAsString());
}
return value;
}
public static boolean canCastFromJson(Type type)
{
String baseType = type.getTypeSignature().getBase();
if (baseType.equals(StandardTypes.BOOLEAN) ||
baseType.equals(StandardTypes.TINYINT) ||
baseType.equals(StandardTypes.SMALLINT) ||
baseType.equals(StandardTypes.INTEGER) ||
baseType.equals(StandardTypes.BIGINT) ||
baseType.equals(StandardTypes.DOUBLE) ||
baseType.equals(StandardTypes.REAL) ||
baseType.equals(StandardTypes.VARCHAR) ||
baseType.equals(StandardTypes.DECIMAL) ||
baseType.equals(StandardTypes.JSON)) {
return true;
}
if (type instanceof ArrayType) {
return canCastFromJson(((ArrayType) type).getElementType());
}
if (type instanceof MapType) {
return isValidJsonObjectKeyType(((MapType) type).getKeyType()) && canCastFromJson(((MapType) type).getValueType());
}
return false;
}
private static boolean isValidJsonObjectKeyType(Type type)
{
String baseType = type.getTypeSignature().getBase();
return baseType.equals(StandardTypes.BOOLEAN) ||
baseType.equals(StandardTypes.TINYINT) ||
baseType.equals(StandardTypes.SMALLINT) ||
baseType.equals(StandardTypes.INTEGER) ||
baseType.equals(StandardTypes.BIGINT) ||
baseType.equals(StandardTypes.DOUBLE) ||
baseType.equals(StandardTypes.REAL) ||
baseType.equals(StandardTypes.DECIMAL) ||
baseType.equals(StandardTypes.VARCHAR);
}
@VisibleForTesting
public static void appendToBlockBuilder(Type type, Object element, BlockBuilder blockBuilder)
{
Class<?> javaType = type.getJavaType();
if (element == null) {
blockBuilder.appendNull();
}
else if (type.getTypeSignature().getBase().equals(StandardTypes.ARRAY) && element instanceof Iterable<?>) {
BlockBuilder subBlockBuilder = blockBuilder.beginBlockEntry();
for (Object subElement : (Iterable<?>) element) {
appendToBlockBuilder(type.getTypeParameters().get(0), subElement, subBlockBuilder);
}
blockBuilder.closeEntry();
}
else if (type.getTypeSignature().getBase().equals(StandardTypes.ROW) && element instanceof Iterable<?>) {
BlockBuilder subBlockBuilder = blockBuilder.beginBlockEntry();
int field = 0;
for (Object subElement : (Iterable<?>) element) {
appendToBlockBuilder(type.getTypeParameters().get(field), subElement, subBlockBuilder);
field++;
}
blockBuilder.closeEntry();
}
else if (type.getTypeSignature().getBase().equals(StandardTypes.MAP) && element instanceof Map<?, ?>) {
BlockBuilder subBlockBuilder = blockBuilder.beginBlockEntry();
for (Map.Entry<?, ?> entry : ((Map<?, ?>) element).entrySet()) {
appendToBlockBuilder(type.getTypeParameters().get(0), entry.getKey(), subBlockBuilder);
appendToBlockBuilder(type.getTypeParameters().get(1), entry.getValue(), subBlockBuilder);
}
blockBuilder.closeEntry();
}
else if (javaType == boolean.class) {
type.writeBoolean(blockBuilder, (Boolean) element);
}
else if (javaType == long.class) {
if (element instanceof SqlDecimal) {
type.writeLong(blockBuilder, ((SqlDecimal) element).getUnscaledValue().longValue());
}
else if (REAL.equals(type)) {
type.writeLong(blockBuilder, floatToRawIntBits(((Number) element).floatValue()));
}
else {
type.writeLong(blockBuilder, ((Number) element).longValue());
}
}
else if (javaType == double.class) {
type.writeDouble(blockBuilder, ((Number) element).doubleValue());
}
else if (javaType == Slice.class) {
if (element instanceof String) {
type.writeSlice(blockBuilder, Slices.utf8Slice(element.toString()));
}
else if (element instanceof byte[]) {
type.writeSlice(blockBuilder, Slices.wrappedBuffer((byte[]) element));
}
else if (element instanceof SqlDecimal) {
type.writeSlice(blockBuilder, Decimals.encodeUnscaledValue(((SqlDecimal) element).getUnscaledValue()));
}
else {
type.writeSlice(blockBuilder, (Slice) element);
}
}
else {
type.writeObject(blockBuilder, element);
}
}
}