| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF 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. |
| */ |
| package org.apache.calcite.avatica.remote; |
| |
| import org.apache.calcite.avatica.AvaticaUtils; |
| import org.apache.calcite.avatica.ColumnMetaData; |
| import org.apache.calcite.avatica.ColumnMetaData.AvaticaType; |
| import org.apache.calcite.avatica.ColumnMetaData.Rep; |
| import org.apache.calcite.avatica.SqlType; |
| import org.apache.calcite.avatica.proto.Common; |
| import org.apache.calcite.avatica.util.ArrayFactoryImpl; |
| import org.apache.calcite.avatica.util.Base64; |
| import org.apache.calcite.avatica.util.ByteString; |
| import org.apache.calcite.avatica.util.DateTimeUtils; |
| |
| import com.fasterxml.jackson.annotation.JsonCreator; |
| import com.fasterxml.jackson.annotation.JsonProperty; |
| import com.google.protobuf.Descriptors.FieldDescriptor; |
| import com.google.protobuf.UnsafeByteOperations; |
| |
| import java.math.BigDecimal; |
| import java.math.BigInteger; |
| import java.sql.Array; |
| import java.sql.ResultSet; |
| import java.sql.SQLException; |
| import java.sql.Time; |
| import java.sql.Timestamp; |
| import java.util.ArrayList; |
| import java.util.Calendar; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Objects; |
| |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| |
| /** Value and type. |
| * |
| * <p>There are 3 representations: |
| * <ul> |
| * <li>JDBC - the representation used by JDBC get and set methods |
| * <li>Serial - suitable for serializing using JSON |
| * <li>Local - used by Calcite for efficient computation |
| * </ul> |
| * |
| * <p>The following table shows the Java type(s) that may represent each SQL |
| * type in each representation. |
| * |
| * <table> |
| * <caption>SQL types and their representations</caption> |
| * <tr> |
| * <th>Type</th> <th>JDBC</th> <th>Serial</th> <th>Local</th> |
| * </tr> |
| * <tr> |
| * <td>BOOLEAN</td> <td>boolean</td> <td>boolean</td> <td>boolean</td> |
| * </tr> |
| * <tr> |
| * <td>BINARY, VARBINARY</td> <td>byte[]</td> |
| * <td>String (base64)</td> <td>{@link ByteString}</td> |
| * </tr> |
| * <tr> |
| * <td>DATE</td> <td>{@link java.sql.Date}</td> |
| * <td>int</td> <td>int</td> |
| * </tr> |
| * <tr> |
| * <td>TIME</td> <td>{@link java.sql.Time}</td> |
| * <td>int</td> <td>int</td> |
| * </tr> |
| * <tr> |
| * <td>DATE</td> <td>{@link java.sql.Timestamp}</td> |
| * <td>long</td> <td>long</td> |
| * </tr> |
| * <tr> |
| * <td>CHAR, VARCHAR</td> |
| * <td>String</td> <td>String</td> <td>String</td> |
| * </tr> |
| * <tr> |
| * <td>TINYINT</td> <td>byte</td> <td>Number</td> <td>byte</td> |
| * </tr> |
| * <tr> |
| * <td>SMALLINT</td> <td>short</td> <td>Number</td> <td>short</td> |
| * </tr> |
| * <tr> |
| * <td>INTEGER</td> <td>int</td> <td>Number</td> <td>int</td> |
| * </tr> |
| * <tr> |
| * <td>BIGINT</td> <td>long</td> <td>Number</td> <td>long</td> |
| * </tr> |
| * <tr> |
| * <td>REAL</td> <td>float</td> <td>Number</td> <td>float</td> |
| * </tr> |
| * <tr> |
| * <td>FLOAT, DOUBLE</td> |
| * <td>double</td> <td>Number</td> <td>double</td> |
| * </tr> |
| * <tr> |
| * <td>DECIMAL</td> |
| * <td>BigDecimal</td> <td>Number</td> <td>BigDecimal</td> |
| * </tr> |
| * <tr> |
| * <td>ARRAY</td> |
| * <td>Array</td> <td>List<Object></td> <td>List<Object></td> |
| * </tr> |
| * </table> |
| * |
| * <p>Note: |
| * |
| * <ul> |
| * <li>The various numeric types (TINYINT, SMALLINT, INTEGER, BIGINT, REAL, |
| * FLOAT, DOUBLE) are represented by {@link Number} in serial format because |
| * JSON numbers are not strongly typed. A {@code float} value {@code 3.0} is |
| * transmitted as {@code 3}, and is therefore decoded as an {@code int}. |
| * |
| * <li>The date-time types (DATE, TIME, TIMESTAMP) are represented in JDBC as |
| * {@link java.sql.Date}, {@link java.sql.Time}, {@link java.sql.Timestamp}, |
| * all sub-classes of {@link java.util.Date}. When they are passed to and |
| * from the server, they are interpreted in terms of a time zone, by default |
| * the current connection's time zone. Their serial and local representations |
| * as {@code int} (days since 1970-01-01 for DATE, milliseconds since |
| * 00:00:00.000 for TIME), and long (milliseconds since 1970-01-01 |
| * 00:00:00.000 for TIMESTAMP) are easier to work with, because it is clear |
| * that time zone is not involved. |
| * |
| * <li>BINARY and VARBINARY values are represented as base64-encoded strings |
| * for serialization over JSON. |
| * </ul> |
| */ |
| public class TypedValue { |
| private static final FieldDescriptor NUMBER_DESCRIPTOR = Common.TypedValue.getDescriptor() |
| .findFieldByNumber(Common.TypedValue.NUMBER_VALUE_FIELD_NUMBER); |
| private static final FieldDescriptor STRING_DESCRIPTOR = Common.TypedValue.getDescriptor() |
| .findFieldByNumber(Common.TypedValue.STRING_VALUE_FIELD_NUMBER); |
| private static final FieldDescriptor BYTES_DESCRIPTOR = Common.TypedValue.getDescriptor() |
| .findFieldByNumber(Common.TypedValue.BYTES_VALUE_FIELD_NUMBER); |
| |
| // If the user sets a `null` Object, it's explicitly null |
| public static final TypedValue EXPLICIT_NULL = |
| new TypedValue(ColumnMetaData.Rep.OBJECT, null); |
| // The user might also implicitly not set a value for a parameter. |
| public static final Common.TypedValue PROTO_IMPLICIT_NULL = |
| Common.TypedValue.newBuilder().setImplicitlyNull(true).build(); |
| |
| /** Type of the value. */ |
| public final ColumnMetaData.Rep type; |
| |
| /** Value. |
| * |
| * <p>Always in a form that can be serialized to JSON by Jackson. |
| * For example, byte arrays are represented as String. */ |
| public final Object value; |
| |
| /** Non-null for ARRAYs, the type of the values stored in the ARRAY. Null for all other cases. */ |
| public final ColumnMetaData.Rep componentType; |
| |
| private TypedValue(ColumnMetaData.Rep rep, Object value) { |
| this(rep, null, value); |
| } |
| |
| private TypedValue(ColumnMetaData.Rep rep, ColumnMetaData.Rep componentType, Object value) { |
| this.type = rep; |
| this.componentType = componentType; |
| this.value = value; |
| assert isSerial(rep, value) : "rep: " + rep + ", value: " + value; |
| } |
| |
| private boolean isSerial(ColumnMetaData.Rep rep, Object value) { |
| if (value == null) { |
| return true; |
| } |
| switch (rep) { |
| case BYTE_STRING: |
| return value instanceof String; |
| case JAVA_SQL_DATE: |
| case JAVA_SQL_TIME: |
| return value instanceof Integer; |
| case JAVA_SQL_TIMESTAMP: |
| case JAVA_UTIL_DATE: |
| return value instanceof Long; |
| default: |
| return true; |
| } |
| } |
| |
| @JsonCreator |
| public static TypedValue create(@JsonProperty("type") String type, |
| @JsonProperty("value") Object value) { |
| if (value == null) { |
| return EXPLICIT_NULL; |
| } |
| ColumnMetaData.Rep rep = ColumnMetaData.Rep.valueOf(type); |
| return ofLocal(rep, serialToLocal(rep, value)); |
| } |
| |
| /** Creates a TypedValue from a value in local representation. */ |
| public static TypedValue ofLocal(ColumnMetaData.Rep rep, Object value) { |
| return new TypedValue(rep, localToSerial(rep, value)); |
| } |
| |
| /** Creates a TypedValue from a value in serial representation. */ |
| public static TypedValue ofSerial(ColumnMetaData.Rep rep, Object value) { |
| return new TypedValue(rep, value); |
| } |
| |
| /** Creates a TypedValue from a value in JDBC representation. */ |
| public static TypedValue ofJdbc(ColumnMetaData.Rep rep, Object value, |
| Calendar calendar) { |
| if (value == null) { |
| return EXPLICIT_NULL; |
| } |
| final Object serialValue; |
| if (ColumnMetaData.Rep.ARRAY == rep) { |
| // Sanity check that we were given an Array |
| if (null != value && !(value instanceof Array)) { |
| throw new IllegalArgumentException("Provided Rep was ARRAY, but the value was " |
| + value.getClass()); |
| } |
| final Array array = (Array) value; |
| try { |
| SqlType type = SqlType.valueOf(array.getBaseType()); |
| serialValue = jdbcToSerial(rep, array, calendar, type); |
| // Because an Array may have null entries, we must always return the non-primitive type |
| // variants of the array values. |
| return new TypedValue(rep, Rep.nonPrimitiveRepOf(type), serialValue); |
| } catch (SQLException e) { |
| throw new RuntimeException("Could not extract Array component type", e); |
| } |
| } else { |
| serialValue = jdbcToSerial(rep, value, calendar); |
| } |
| return new TypedValue(rep, serialValue); |
| } |
| |
| /** Creates a TypedValue from a value in JDBC representation, |
| * deducing its type. */ |
| public static TypedValue ofJdbc(Object value, Calendar calendar) { |
| if (value == null) { |
| return EXPLICIT_NULL; |
| } |
| final ColumnMetaData.Rep rep = ColumnMetaData.Rep.of(value.getClass()); |
| return new TypedValue(rep, jdbcToSerial(rep, value, calendar)); |
| } |
| |
| /** Converts the value into the local representation. |
| * |
| * <p>For example, a byte string is represented as a {@link ByteString}; |
| * a long is represented as a {@link Long} (not just some {@link Number}). |
| */ |
| public Object toLocal() { |
| if (value == null) { |
| return null; |
| } |
| return serialToLocal(type, value); |
| } |
| |
| /** Converts a value to the exact type required for the given |
| * representation. */ |
| private static Object serialToLocal(ColumnMetaData.Rep rep, Object value) { |
| assert value != null; |
| if (value.getClass() == rep.clazz) { |
| return value; |
| } |
| switch (rep) { |
| case BYTE: |
| return ((Number) value).byteValue(); |
| case SHORT: |
| return ((Number) value).shortValue(); |
| case INTEGER: |
| case JAVA_SQL_DATE: |
| case JAVA_SQL_TIME: |
| return ((Number) value).intValue(); |
| case LONG: |
| case JAVA_UTIL_DATE: |
| case JAVA_SQL_TIMESTAMP: |
| return ((Number) value).longValue(); |
| case FLOAT: |
| return ((Number) value).floatValue(); |
| case DOUBLE: |
| return ((Number) value).doubleValue(); |
| case NUMBER: |
| return value instanceof BigDecimal ? value |
| : value instanceof BigInteger ? new BigDecimal((BigInteger) value) |
| : value instanceof Double ? new BigDecimal((Double) value) |
| : value instanceof Float ? new BigDecimal((Float) value) |
| : new BigDecimal(((Number) value).longValue()); |
| case BYTE_STRING: |
| return ByteString.ofBase64((String) value); |
| case ARRAY: |
| //List<Object> |
| return value; |
| default: |
| throw new IllegalArgumentException("cannot convert " + value + " (" |
| + value.getClass() + ") to " + rep); |
| } |
| } |
| |
| /** Converts the value into the JDBC representation. |
| * |
| * <p>For example, a byte string is represented as a {@link ByteString}; |
| * a long is represented as a {@link Long} (not just some {@link Number}). |
| */ |
| public Object toJdbc(Calendar calendar) { |
| if (value == null) { |
| return null; |
| } |
| return serialToJdbc(type, componentType, value, calendar); |
| } |
| |
| /** |
| * Converts the given value from serial form to JDBC form. |
| * |
| * @param type The type of the value |
| * @param value The value |
| * @param calendar A calendar instance |
| * @return The JDBC representation of the value. |
| */ |
| private static Object serialToJdbc(ColumnMetaData.Rep type, ColumnMetaData.Rep componentRep, |
| Object value, Calendar calendar) { |
| switch (type) { |
| case BYTE_STRING: |
| return ByteString.ofBase64((String) value).getBytes(); |
| case JAVA_UTIL_DATE: |
| return new java.util.Date(adjust((Number) value, calendar)); |
| case JAVA_SQL_DATE: |
| return new java.sql.Date( |
| adjust(((Number) value).longValue() * DateTimeUtils.MILLIS_PER_DAY, |
| calendar)); |
| case JAVA_SQL_TIME: |
| return new java.sql.Time(adjust((Number) value, calendar)); |
| case JAVA_SQL_TIMESTAMP: |
| return new java.sql.Timestamp(adjust((Number) value, calendar)); |
| case ARRAY: |
| if (null == value) { |
| return null; |
| } |
| final List<?> list = (List<?>) value; |
| final List<Object> copy = new ArrayList<>(list.size()); |
| // Copy the list from the serial representation to a JDBC representation |
| for (Object o : list) { |
| if (null == o) { |
| copy.add(null); |
| } else if (o instanceof TypedValue) { |
| // Protobuf can maintain the TypedValue hierarchy to simplify things |
| copy.add(((TypedValue) o).toJdbc(calendar)); |
| } else { |
| // We can't get the above recursion with the JSON serialization |
| copy.add(serialToJdbc(componentRep, null, o, calendar)); |
| } |
| } |
| if (componentRep == null && list.size() > 0) { |
| componentRep = ((TypedValue) list.get(0)).type; |
| if (componentRep == null) { |
| throw new RuntimeException("ComponentRep of element must not be null for ARRAYs"); |
| } |
| } |
| AvaticaType elementType = new AvaticaType(componentRep.typeId, componentRep.name(), |
| componentRep); |
| return new ArrayFactoryImpl(calendar.getTimeZone()).createArray(elementType, copy); |
| default: |
| return serialToLocal(type, value); |
| } |
| } |
| |
| private static long adjust(Number number, Calendar calendar) { |
| long t = number.longValue(); |
| if (calendar != null) { |
| t -= calendar.getTimeZone().getOffset(t); |
| } |
| return t; |
| } |
| |
| private static Object jdbcToSerial(ColumnMetaData.Rep rep, Object value, |
| Calendar calendar) { |
| return jdbcToSerial(rep, value, calendar, null); |
| } |
| |
| /** Converts a value from JDBC format to a type that can be serialized as |
| * JSON. */ |
| private static Object jdbcToSerial(ColumnMetaData.Rep rep, Object value, |
| Calendar calendar, SqlType componentType) { |
| if (null == value) { |
| return null; |
| } |
| switch (rep) { |
| case BYTE_STRING: |
| return new ByteString((byte[]) value).toBase64String(); |
| case JAVA_UTIL_DATE: |
| case JAVA_SQL_TIMESTAMP: |
| case JAVA_SQL_DATE: |
| case JAVA_SQL_TIME: |
| long t = ((Date) value).getTime(); |
| if (calendar != null) { |
| t += calendar.getTimeZone().getOffset(t); |
| } |
| switch (rep) { |
| case JAVA_SQL_DATE: |
| return (int) DateTimeUtils.floorDiv(t, DateTimeUtils.MILLIS_PER_DAY); |
| case JAVA_SQL_TIME: |
| return (int) DateTimeUtils.floorMod(t, DateTimeUtils.MILLIS_PER_DAY); |
| default: |
| return t; |
| } |
| case ARRAY: |
| Array array = (Array) value; |
| Objects.requireNonNull(componentType, "Component Type must not be null for ARRAYs"); |
| try { |
| switch (componentType) { |
| case BINARY: |
| case VARBINARY: |
| case LONGVARBINARY: |
| Object[] byteStrings = (Object[]) array.getArray(); |
| List<String> convertedStrings = new ArrayList<>(byteStrings.length); |
| for (Object byteString : byteStrings) { |
| convertedStrings.add( |
| (String) jdbcToSerial(Rep.BYTE_STRING, byteString, calendar, null)); |
| } |
| return convertedStrings; |
| case DATE: |
| case TIME: |
| Object[] dates = (Object[]) array.getArray(); |
| List<Integer> serializedDates = new ArrayList<>(dates.length); |
| for (Object obj : dates) { |
| Date date = (Date) obj; |
| if (null == obj) { |
| serializedDates.add(null); |
| } else if (componentType == SqlType.DATE) { |
| serializedDates.add((int) jdbcToSerial(Rep.JAVA_SQL_DATE, date, calendar, null)); |
| } else if (componentType == SqlType.TIME) { |
| serializedDates.add((int) jdbcToSerial(Rep.JAVA_SQL_TIME, date, calendar, null)); |
| } else { |
| throw new RuntimeException("Unexpected type: " + componentType); |
| } |
| } |
| return serializedDates; |
| case TIMESTAMP: |
| Object[] timestamps = (Object[]) array.getArray(); |
| List<Long> serializedTimestamps = new ArrayList<>(timestamps.length); |
| for (Object obj : timestamps) { |
| Timestamp timestamp = (Timestamp) obj; |
| if (null == obj) { |
| serializedTimestamps.add(null); |
| } else { |
| serializedTimestamps.add( |
| (long) jdbcToSerial(Rep.JAVA_SQL_TIMESTAMP, timestamp, calendar, null)); |
| } |
| } |
| return serializedTimestamps; |
| default: |
| // Either a primitive array or Object[], converted into List<Object> |
| return AvaticaUtils.primitiveList(array.getArray()); |
| } |
| } catch (SQLException e) { |
| throw new RuntimeException("Could not obtain base array object", e); |
| } |
| default: |
| return value; |
| } |
| } |
| |
| /** Converts a value from internal format to a type that can be serialized |
| * as JSON. */ |
| private static Object localToSerial(ColumnMetaData.Rep rep, Object value) { |
| switch (rep) { |
| case BYTE_STRING: |
| return ((ByteString) value).toBase64String(); |
| default: |
| return value; |
| } |
| } |
| |
| /** Converts a list of {@code TypedValue} to a list of values. */ |
| public static List<Object> values(List<TypedValue> typedValues) { |
| final List<Object> list = new ArrayList<>(); |
| for (TypedValue typedValue : typedValues) { |
| list.add(typedValue.toLocal()); |
| } |
| return list; |
| } |
| |
| /** |
| * Creates a protocol buffer equivalent object for <code>this</code>. |
| * @return A protobuf TypedValue equivalent for <code>this</code> |
| */ |
| public Common.TypedValue toProto() { |
| final Common.TypedValue.Builder builder = Common.TypedValue.newBuilder(); |
| // This isn't a static method, therefore we have a non-null TypedValue. Thus, this message |
| // cannot be implicitly null |
| builder.setImplicitlyNull(false); |
| |
| Common.Rep protoRep = type.toProto(); |
| // Protobuf has an explicit BIG_DECIMAL representation enum value. |
| if (Common.Rep.NUMBER == protoRep && value instanceof BigDecimal) { |
| protoRep = Common.Rep.BIG_DECIMAL; |
| } else if (Common.Rep.ARRAY == protoRep) { |
| // This column value is an Array (many TypedValue's) |
| builder.setType(Common.Rep.ARRAY); |
| // Get the array component's type |
| Common.Rep protoComponentRep = componentType.toProto(); |
| // Set the array's component on the message |
| builder.setComponentType(protoComponentRep); |
| // Serialize that array into the builder |
| @SuppressWarnings("unchecked") |
| List<Object> list = (List<Object>) value; |
| return serializeArray(list, builder, protoComponentRep); |
| } |
| |
| // Serialize the type into the protobuf |
| writeToProtoWithType(builder, value, protoRep); |
| |
| return builder.build(); |
| } |
| |
| Common.TypedValue serializeArray(List<Object> list, Common.TypedValue.Builder builder, |
| Common.Rep protoArrayComponentRep) { |
| for (Object element : list) { |
| if (element instanceof List) { |
| // We have a list of lists: recursively build up the protobuf |
| @SuppressWarnings("unchecked") |
| List<Object> subList = (List<Object>) element; |
| Common.TypedValue.Builder subListBuilder = Common.TypedValue.newBuilder(); |
| // This is "technically" an array, but we just persist the underlying component type |
| subListBuilder.setType(protoArrayComponentRep); |
| Common.TypedValue protoSubList = serializeArray(subList, subListBuilder, |
| protoArrayComponentRep); |
| builder.addArrayValue(protoSubList); |
| } else { |
| // We have a list of "scalars", just serialize the value |
| Common.TypedValue.Builder elementBuilder = Common.TypedValue.newBuilder(); |
| if (null == element) { |
| writeToProtoWithType(elementBuilder, null, Common.Rep.NULL); |
| } else { |
| writeToProtoWithType(elementBuilder, element, protoArrayComponentRep); |
| } |
| builder.addArrayValue(elementBuilder.build()); |
| } |
| } |
| return builder.build(); |
| } |
| |
| private static void writeToProtoWithType(Common.TypedValue.Builder builder, Object o, |
| Common.Rep type) { |
| builder.setType(type); |
| |
| switch (type) { |
| case BOOLEAN: |
| case PRIMITIVE_BOOLEAN: |
| builder.setBoolValue((boolean) o); |
| return; |
| case BYTE_STRING: |
| byte[] bytes; |
| // Serial representation is b64. We don't need to do that for protobuf |
| if (o instanceof String) { |
| // Backwards compatibility for client CALCITE-1209 |
| builder.setStringValue((String) o); |
| // Assume strings are already b64 encoded |
| bytes = ByteString.parseBase64((String) o); |
| } else { |
| // Backwards compatibility for client CALCITE-1209 |
| builder.setStringValue(Base64.encodeBytes((byte[]) o)); |
| // Use the byte array |
| bytes = (byte[]) o; |
| } |
| builder.setBytesValue(UnsafeByteOperations.unsafeWrap(bytes)); |
| return; |
| case STRING: |
| String s; |
| if (DateTimeUtils.isOffsetDateTime(o)) { |
| s = DateTimeUtils.offsetDateTimeValue(o); |
| } else { |
| s = (String) o; |
| } |
| builder.setStringValueBytes(UnsafeByteOperations.unsafeWrap(s.getBytes(UTF_8))); |
| return; |
| case PRIMITIVE_CHAR: |
| case CHARACTER: |
| builder.setStringValue(Character.toString((char) o)); |
| return; |
| case BYTE: |
| case PRIMITIVE_BYTE: |
| builder.setNumberValue(Byte.valueOf((byte) o).longValue()); |
| return; |
| case DOUBLE: |
| case PRIMITIVE_DOUBLE: |
| builder.setDoubleValue((double) o); |
| return; |
| case FLOAT: |
| case PRIMITIVE_FLOAT: |
| builder.setNumberValue(Float.floatToIntBits((float) o)); |
| return; |
| case INTEGER: |
| case PRIMITIVE_INT: |
| builder.setNumberValue(Integer.valueOf((int) o).longValue()); |
| return; |
| case PRIMITIVE_SHORT: |
| case SHORT: |
| builder.setNumberValue(Short.valueOf((short) o).longValue()); |
| return; |
| case LONG: |
| case PRIMITIVE_LONG: |
| builder.setNumberValue((long) o); |
| return; |
| case JAVA_SQL_DATE: |
| case JAVA_SQL_TIME: |
| long sqlDateOrTime; |
| if (o instanceof java.sql.Date) { |
| sqlDateOrTime = ((java.sql.Date) o).getTime(); |
| } else if (o instanceof java.sql.Time) { |
| sqlDateOrTime = ((java.sql.Time) o).getTime(); |
| } else if (o instanceof Integer) { |
| sqlDateOrTime = ((Integer) o).longValue(); |
| } else { |
| sqlDateOrTime = (long) o; |
| } |
| // Persisted as numbers |
| builder.setNumberValue(sqlDateOrTime); |
| return; |
| case JAVA_SQL_TIMESTAMP: |
| case JAVA_UTIL_DATE: |
| long sqlTimestampOrUtilDate; |
| if (o instanceof java.sql.Timestamp) { |
| sqlTimestampOrUtilDate = ((java.sql.Timestamp) o).getTime(); |
| } else if (o instanceof java.util.Date) { |
| sqlTimestampOrUtilDate = ((java.util.Date) o).getTime(); |
| } else { |
| sqlTimestampOrUtilDate = (long) o; |
| } |
| // Persisted as longs |
| builder.setNumberValue(sqlTimestampOrUtilDate); |
| return; |
| case BIG_INTEGER: |
| byte[] byteRep = ((BigInteger) o).toByteArray(); |
| builder.setBytesValue(com.google.protobuf.ByteString.copyFrom(byteRep)); |
| return; |
| case BIG_DECIMAL: |
| final BigDecimal bigDecimal = (BigDecimal) o; |
| builder.setStringValue(bigDecimal.toString()); |
| return; |
| case NUMBER: |
| builder.setNumberValue(((Number) o).longValue()); |
| return; |
| case NULL: |
| builder.setNull(true); |
| return; |
| case OBJECT: |
| if (null == o) { |
| // We can persist a null value through easily |
| builder.setNull(true); |
| return; |
| } |
| // Intentional fall-through to RTE because we can't serialize something we have no type |
| // insight into. |
| // fall through |
| case UNRECOGNIZED: |
| // Fail? |
| throw new RuntimeException("Unhandled value: " + type + " " + o.getClass()); |
| default: |
| // Fail? |
| throw new RuntimeException("Unknown serialized type: " + type); |
| } |
| } |
| |
| /** |
| * Constructs a {@link TypedValue} from the protocol buffer representation. |
| * |
| * @param proto The protobuf Typedvalue |
| * @return A {@link TypedValue} instance |
| */ |
| public static TypedValue fromProto(Common.TypedValue proto) { |
| ColumnMetaData.Rep rep = ColumnMetaData.Rep.fromProto(proto.getType()); |
| ColumnMetaData.Rep componentRep = ColumnMetaData.Rep.fromProto(proto.getComponentType()); |
| Object value = getSerialFromProto(proto); |
| |
| return new TypedValue(rep, componentRep, value); |
| } |
| |
| /** |
| * Converts the serialized value into the appropriate primitive/object. |
| * |
| * @param protoValue The serialized TypedValue. |
| * @return The appropriate concrete type for the parameter value (as an Object). |
| */ |
| public static Object getSerialFromProto(Common.TypedValue protoValue) { |
| // Deserialize the value again |
| switch (protoValue.getType()) { |
| case BOOLEAN: |
| case PRIMITIVE_BOOLEAN: |
| return protoValue.getBoolValue(); |
| case BYTE_STRING: |
| if (protoValue.hasField(STRING_DESCRIPTOR) && !protoValue.hasField(BYTES_DESCRIPTOR)) { |
| // Prior to CALCITE-1103, clients would send b64 strings for bytes instead of using the |
| // native byte format. The value we need to provide as the local format for TypedValue |
| // is directly accessible via the Protobuf representation. Both fields are sent by the |
| // server to support older clients, so only parse the string value when it is alone. |
| return protoValue.getStringValue(); |
| } |
| // TypedValue is still going to expect a b64string for BYTE_STRING even though we sent it |
| // across the wire natively as bytes. Return it as b64. |
| return (new ByteString(protoValue.getBytesValue().toByteArray())).toBase64String(); |
| case STRING: |
| return protoValue.getStringValue(); |
| case PRIMITIVE_CHAR: |
| case CHARACTER: |
| return protoValue.getStringValue().charAt(0); |
| case BYTE: |
| case PRIMITIVE_BYTE: |
| return Long.valueOf(protoValue.getNumberValue()).byteValue(); |
| case DOUBLE: |
| case PRIMITIVE_DOUBLE: |
| return protoValue.getDoubleValue(); |
| case FLOAT: |
| case PRIMITIVE_FLOAT: |
| return Float.intBitsToFloat((int) protoValue.getNumberValue()); |
| case INTEGER: |
| case PRIMITIVE_INT: |
| return Long.valueOf(protoValue.getNumberValue()).intValue(); |
| case PRIMITIVE_SHORT: |
| case SHORT: |
| return Long.valueOf(protoValue.getNumberValue()).shortValue(); |
| case LONG: |
| case PRIMITIVE_LONG: |
| return Long.valueOf(protoValue.getNumberValue()); |
| case JAVA_SQL_DATE: |
| case JAVA_SQL_TIME: |
| return Long.valueOf(protoValue.getNumberValue()).intValue(); |
| case JAVA_SQL_TIMESTAMP: |
| case JAVA_UTIL_DATE: |
| return protoValue.getNumberValue(); |
| case BIG_INTEGER: |
| return new BigInteger(protoValue.getBytesValue().toByteArray()); |
| case BIG_DECIMAL: |
| // CALCITE-1103 shifts BigDecimals to be serialized as strings. |
| if (protoValue.hasField(NUMBER_DESCRIPTOR)) { |
| // This is the old (broken) style. |
| BigInteger bigInt = new BigInteger(protoValue.getBytesValue().toByteArray()); |
| return new BigDecimal(bigInt, (int) protoValue.getNumberValue()); |
| } |
| return new BigDecimal(protoValue.getStringValueBytes().toStringUtf8()); |
| case NUMBER: |
| return Long.valueOf(protoValue.getNumberValue()); |
| case NULL: |
| return null; |
| case ARRAY: |
| final List<Common.TypedValue> protoList = protoValue.getArrayValueList(); |
| final List<Object> list = new ArrayList<>(protoList.size()); |
| for (Common.TypedValue protoElement : protoList) { |
| // Deserialize the TypedValue protobuf into the JDBC type |
| TypedValue listElement = TypedValue.fromProto(protoElement); |
| // Must preserve the TypedValue so serial/jdbc/local conversion can work as expected |
| list.add(listElement); |
| } |
| return list; |
| case OBJECT: |
| if (protoValue.getNull()) { |
| return null; |
| } |
| // Intentional fall through to RTE. If we sent an object over the wire, it could only |
| // possibly be null (at this point). Anything else has to be an error. |
| // fall through |
| case UNRECOGNIZED: |
| // Fail? |
| throw new RuntimeException("Unhandled type: " + protoValue.getType()); |
| default: |
| // Fail? |
| throw new RuntimeException("Unknown type: " + protoValue.getType()); |
| } |
| } |
| |
| /** |
| * Writes the given object into the Protobuf representation of a TypedValue. The object is |
| * serialized given the type of that object, mapping it to the appropriate representation. |
| * |
| * @param builder The TypedValue protobuf builder |
| * @param o The object (value) |
| */ |
| public static Common.Rep toProto(Common.TypedValue.Builder builder, Object o) { |
| // Numbers |
| if (o instanceof Byte) { |
| writeToProtoWithType(builder, o, Common.Rep.BYTE); |
| return Common.Rep.BYTE; |
| } else if (o instanceof Short) { |
| writeToProtoWithType(builder, o, Common.Rep.SHORT); |
| return Common.Rep.SHORT; |
| } else if (o instanceof Integer) { |
| writeToProtoWithType(builder, o, Common.Rep.INTEGER); |
| return Common.Rep.INTEGER; |
| } else if (o instanceof Long) { |
| writeToProtoWithType(builder, o, Common.Rep.LONG); |
| return Common.Rep.LONG; |
| } else if (o instanceof Double) { |
| writeToProtoWithType(builder, o, Common.Rep.DOUBLE); |
| return Common.Rep.DOUBLE; |
| } else if (o instanceof Float) { |
| writeToProtoWithType(builder, o, Common.Rep.FLOAT); |
| return Common.Rep.FLOAT; |
| } else if (o instanceof BigDecimal) { |
| writeToProtoWithType(builder, o, Common.Rep.BIG_DECIMAL); |
| return Common.Rep.BIG_DECIMAL; |
| // Strings |
| } else if (o instanceof String) { |
| writeToProtoWithType(builder, o, Common.Rep.STRING); |
| return Common.Rep.STRING; |
| } else if (o instanceof Character) { |
| writeToProtoWithType(builder, o, Common.Rep.CHARACTER); |
| return Common.Rep.CHARACTER; |
| // Bytes |
| } else if (o instanceof byte[]) { |
| writeToProtoWithType(builder, o, Common.Rep.BYTE_STRING); |
| return Common.Rep.BYTE_STRING; |
| // Boolean |
| } else if (o instanceof Boolean) { |
| writeToProtoWithType(builder, o, Common.Rep.BOOLEAN); |
| return Common.Rep.BOOLEAN; |
| } else if (o instanceof Timestamp) { |
| writeToProtoWithType(builder, o, Common.Rep.JAVA_SQL_TIMESTAMP); |
| return Common.Rep.JAVA_SQL_TIMESTAMP; |
| } else if (o instanceof java.sql.Date) { |
| writeToProtoWithType(builder, o, Common.Rep.JAVA_SQL_DATE); |
| return Common.Rep.JAVA_SQL_DATE; |
| } else if (o instanceof Time) { |
| writeToProtoWithType(builder, o, Common.Rep.JAVA_SQL_TIME); |
| return Common.Rep.JAVA_SQL_TIME; |
| } else if (DateTimeUtils.isOffsetDateTime(o)) { |
| // SQL type is TIMESTAMP WITH TIMEZONE. Transmit as a string. |
| writeToProtoWithType(builder, o, Common.Rep.STRING); |
| return Common.Rep.STRING; |
| } else if (o instanceof List) { |
| // Treat a List as an Array |
| builder.setType(Common.Rep.ARRAY); |
| builder.setComponentType(Common.Rep.OBJECT); |
| boolean setComponentType = false; |
| for (Object listElement : (List<?>) o) { |
| Common.TypedValue.Builder listElementBuilder = Common.TypedValue.newBuilder(); |
| // Recurse on each list element |
| Common.Rep componentRep = toProto(listElementBuilder, listElement); |
| if (!setComponentType) { |
| if (Common.Rep.NULL != componentRep) { |
| builder.setComponentType(componentRep); |
| } |
| setComponentType = true; |
| } |
| builder.addArrayValue(listElementBuilder.build()); |
| } |
| return Common.Rep.ARRAY; |
| } else if (o instanceof Array) { |
| builder.setType(Common.Rep.ARRAY); |
| Array a = (Array) o; |
| try { |
| ResultSet rs = a.getResultSet(); |
| builder.setComponentType(Common.Rep.OBJECT); |
| boolean setComponentType = false; |
| while (rs.next()) { |
| Common.TypedValue.Builder listElementBuilder = Common.TypedValue.newBuilder(); |
| Object arrayValue = rs.getObject(2); |
| Common.Rep componentRep = toProto(listElementBuilder, arrayValue); |
| if (!setComponentType) { |
| if (Common.Rep.NULL != componentRep) { |
| builder.setComponentType(componentRep); |
| } |
| setComponentType = true; |
| } |
| builder.addArrayValue(listElementBuilder.build()); |
| } |
| } catch (SQLException e) { |
| throw new RuntimeException("Could not serialize ARRAY", e); |
| } |
| return Common.Rep.ARRAY; |
| } else if (null == o) { |
| writeToProtoWithType(builder, o, Common.Rep.NULL); |
| return Common.Rep.NULL; |
| // Unhandled |
| } else { |
| throw new RuntimeException("Unhandled type in Frame: " + o.getClass()); |
| } |
| } |
| |
| /** |
| * Extracts the JDBC value from protobuf-TypedValue representation. |
| * |
| * @param protoValue Protobuf TypedValue |
| * @param calendar Instance of a calendar |
| * @return The JDBC representation of this TypedValue |
| */ |
| public static Object protoToJdbc(Common.TypedValue protoValue, Calendar calendar) { |
| Object o = getSerialFromProto(Objects.requireNonNull(protoValue)); |
| // Shortcircuit the null |
| if (null == o) { |
| return o; |
| } |
| return serialToJdbc(Rep.fromProto(protoValue.getType()), null, o, calendar); |
| } |
| |
| @Override public int hashCode() { |
| final int prime = 31; |
| int result = 1; |
| result = prime * result + ((type == null) ? 0 : type.hashCode()); |
| result = prime * result + ((value == null) ? 0 : value.hashCode()); |
| return result; |
| } |
| |
| @Override public boolean equals(Object o) { |
| if (o == this) { |
| return true; |
| } |
| if (o instanceof TypedValue) { |
| TypedValue other = (TypedValue) o; |
| |
| if (type != other.type) { |
| return false; |
| } |
| |
| if (null == value) { |
| return null == other.value; |
| } |
| |
| return value.equals(other.value); |
| } |
| |
| return false; |
| } |
| } |
| |
| // End TypedValue.java |