| /* | |
| * 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.parquet.schema; | |
| import static java.util.concurrent.TimeUnit.HOURS; | |
| import static java.util.concurrent.TimeUnit.MICROSECONDS; | |
| import static java.util.concurrent.TimeUnit.MILLISECONDS; | |
| import static java.util.concurrent.TimeUnit.MINUTES; | |
| import static java.util.concurrent.TimeUnit.NANOSECONDS; | |
| import static java.util.concurrent.TimeUnit.SECONDS; | |
| import java.math.BigDecimal; | |
| import java.math.BigInteger; | |
| import java.nio.ByteBuffer; | |
| import java.nio.ByteOrder; | |
| import java.time.DateTimeException; | |
| import java.time.Instant; | |
| import java.time.ZoneOffset; | |
| import java.time.format.DateTimeFormatter; | |
| import java.util.concurrent.TimeUnit; | |
| import org.apache.parquet.bytes.BytesUtils; | |
| import org.apache.parquet.io.api.Binary; | |
| import org.locationtech.jts.geom.Geometry; | |
| import org.locationtech.jts.io.ParseException; | |
| import org.locationtech.jts.io.WKBReader; | |
| /** | |
| * Class that provides string representations for the primitive values. These string values are to be used for | |
| * logging/debugging purposes. The method {@code stringify} is overloaded for each primitive types. The overloaded | |
| * methods not implemented for the related types throw {@link UnsupportedOperationException}. | |
| */ | |
| public abstract class PrimitiveStringifier { | |
| private final String name; | |
| private PrimitiveStringifier(String name) { | |
| this.name = name; | |
| } | |
| @Override | |
| public final String toString() { | |
| return name; | |
| } | |
| /** | |
| * @param value the value to be stringified | |
| * @return the string representation for {@code value} | |
| * @throws UnsupportedOperationException if value type is not supported by this stringifier | |
| */ | |
| public String stringify(boolean value) { | |
| throw new UnsupportedOperationException( | |
| "stringify(boolean) was called on a non-boolean stringifier: " + toString()); | |
| } | |
| /** | |
| * @param value the value to be stringified | |
| * @return the string representation for {@code value} | |
| * @throws UnsupportedOperationException if value type is not supported by this stringifier | |
| */ | |
| public String stringify(int value) { | |
| throw new UnsupportedOperationException("stringify(int) was called on a non-int stringifier: " + toString()); | |
| } | |
| /** | |
| * @param value the value to be stringified | |
| * @return the string representation for {@code value} | |
| * @throws UnsupportedOperationException if value type is not supported by this stringifier | |
| */ | |
| public String stringify(long value) { | |
| throw new UnsupportedOperationException("stringify(long) was called on a non-long stringifier: " + toString()); | |
| } | |
| /** | |
| * @param value the value to be stringified | |
| * @return the string representation for {@code value} | |
| * @throws UnsupportedOperationException if value type is not supported by this stringifier | |
| */ | |
| public String stringify(float value) { | |
| throw new UnsupportedOperationException( | |
| "stringify(float) was called on a non-float stringifier: " + toString()); | |
| } | |
| /** | |
| * @param value the value to be stringified | |
| * @return the string representation for {@code value} | |
| * @throws UnsupportedOperationException if value type is not supported by this stringifier | |
| */ | |
| public String stringify(double value) { | |
| throw new UnsupportedOperationException( | |
| "stringify(double) was called on a non-double stringifier: " + toString()); | |
| } | |
| /** | |
| * @param value the value to be stringified | |
| * @return the string representation for {@code value} | |
| * @throws UnsupportedOperationException if value type is not supported by this stringifier | |
| */ | |
| public String stringify(Binary value) { | |
| throw new UnsupportedOperationException( | |
| "stringify(Binary) was called on a non-Binary stringifier: " + toString()); | |
| } | |
| private static final String BINARY_NULL = "null"; | |
| private static final String BINARY_HEXA_PREFIX = "0x"; | |
| private static final String BINARY_INVALID = "<INVALID>"; | |
| abstract static class BinaryStringifierBase extends PrimitiveStringifier { | |
| private BinaryStringifierBase(String name) { | |
| super(name); | |
| } | |
| @Override | |
| public final String stringify(Binary value) { | |
| return value == null ? BINARY_NULL : stringifyNotNull(value); | |
| } | |
| abstract String stringifyNotNull(Binary value); | |
| } | |
| static final PrimitiveStringifier DEFAULT_STRINGIFIER = new BinaryStringifierBase("DEFAULT_STRINGIFIER") { | |
| private final char[] digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; | |
| @Override | |
| public String stringify(boolean value) { | |
| return Boolean.toString(value); | |
| } | |
| @Override | |
| public String stringify(int value) { | |
| return Integer.toString(value); | |
| } | |
| @Override | |
| public String stringify(long value) { | |
| return Long.toString(value); | |
| } | |
| @Override | |
| public String stringify(float value) { | |
| return Float.toString(value); | |
| } | |
| @Override | |
| public String stringify(double value) { | |
| return Double.toString(value); | |
| } | |
| @Override | |
| String stringifyNotNull(Binary value) { | |
| ByteBuffer buffer = value.toByteBuffer(); | |
| StringBuilder builder = new StringBuilder(2 + buffer.remaining() * 2); | |
| builder.append(BINARY_HEXA_PREFIX); | |
| for (int i = buffer.position(), n = buffer.limit(); i < n; ++i) { | |
| byte b = buffer.get(i); | |
| builder.append(digits[(b >>> 4) & 0x0F]); | |
| builder.append(digits[b & 0x0F]); | |
| } | |
| return builder.toString(); | |
| } | |
| }; | |
| static final PrimitiveStringifier UNSIGNED_STRINGIFIER = new PrimitiveStringifier("UNSIGNED_STRINGIFIER") { | |
| private static final long INT_MASK = 0x00000000FFFFFFFFl; | |
| // Implemented based on com.google.common.primitives.UnsignedInts.toString(int, int) | |
| @Override | |
| public String stringify(int value) { | |
| return Long.toString(value & INT_MASK); | |
| } | |
| // Implemented based on com.google.common.primitives.UnsignedLongs.toString(long, int) | |
| @Override | |
| public String stringify(long value) { | |
| if (value == 0) { | |
| // Simply return "0" | |
| return "0"; | |
| } else if (value > 0) { | |
| return Long.toString(value); | |
| } else { | |
| char[] buf = new char[64]; | |
| int i = buf.length; | |
| // Split x into high-order and low-order halves. | |
| // Individual digits are generated from the bottom half into which | |
| // bits are moved continuously from the top half. | |
| long top = value >>> 32; | |
| long bot = (value & INT_MASK) + ((top % 10) << 32); | |
| top /= 10; | |
| while ((bot > 0) || (top > 0)) { | |
| buf[--i] = Character.forDigit((int) (bot % 10), 10); | |
| bot = (bot / 10) + ((top % 10) << 32); | |
| top /= 10; | |
| } | |
| // Generate string | |
| return new String(buf, i, buf.length - i); | |
| } | |
| } | |
| }; | |
| static final PrimitiveStringifier UTF8_STRINGIFIER = new BinaryStringifierBase("UTF8_STRINGIFIER") { | |
| @Override | |
| String stringifyNotNull(Binary value) { | |
| return value.toStringUsingUTF8(); | |
| } | |
| }; | |
| static final PrimitiveStringifier INTERVAL_STRINGIFIER = new BinaryStringifierBase("INTERVAL_STRINGIFIER") { | |
| @Override | |
| String stringifyNotNull(Binary value) { | |
| if (value.length() != 12) { | |
| return BINARY_INVALID; | |
| } | |
| ByteBuffer buffer = value.toByteBuffer().order(ByteOrder.LITTLE_ENDIAN); | |
| int pos = buffer.position(); | |
| String months = UNSIGNED_STRINGIFIER.stringify(buffer.getInt(pos)); | |
| String days = UNSIGNED_STRINGIFIER.stringify(buffer.getInt(pos + 4)); | |
| String millis = UNSIGNED_STRINGIFIER.stringify(buffer.getInt(pos + 8)); | |
| return "interval(" + months + " months, " + days + " days, " + millis + " millis)"; | |
| } | |
| }; | |
| private static class DateStringifier extends PrimitiveStringifier { | |
| private final DateTimeFormatter formatter; | |
| private DateStringifier(String name, String format) { | |
| super(name); | |
| formatter = DateTimeFormatter.ofPattern(format).withZone(ZoneOffset.UTC); | |
| } | |
| @Override | |
| public String stringify(int value) { | |
| return toFormattedString(getInstant(value)); | |
| } | |
| @Override | |
| public String stringify(long value) { | |
| return toFormattedString(getInstant(value)); | |
| } | |
| final String toFormattedString(Instant instant) { | |
| return formatter.format(instant); | |
| } | |
| Instant getInstant(int value) { | |
| // throw the related unsupported exception | |
| super.stringify(value); | |
| return null; | |
| } | |
| Instant getInstant(long value) { | |
| // throw the related unsupported exception | |
| super.stringify(value); | |
| return null; | |
| } | |
| } | |
| /** | |
| * Stringifier implementation for timestamps that handles both int64 and FLBA(12) carriers. | |
| * Values outside of Instant's supported range render as raw integers rather than human-readable | |
| * timestamps. | |
| */ | |
| private abstract static class TimestampStringifier extends DateStringifier { | |
| private TimestampStringifier(String name, String format) { | |
| super(name, format); | |
| } | |
| @Override | |
| public String stringify(Binary value) { | |
| if (value == null) { | |
| return BINARY_NULL; | |
| } | |
| BigInteger units = new BigInteger(BytesUtils.reverse(value.getBytesUnsafe())); | |
| try { | |
| return toFormattedString(getInstant(units)); | |
| } catch (ArithmeticException | DateTimeException e) { | |
| return units.toString(); | |
| } | |
| } | |
| abstract Instant getInstant(BigInteger value); | |
| } | |
| static final PrimitiveStringifier DATE_STRINGIFIER = new DateStringifier("DATE_STRINGIFIER", "yyyy-MM-dd") { | |
| @Override | |
| Instant getInstant(int value) { | |
| return Instant.ofEpochMilli(TimeUnit.DAYS.toMillis(value)); | |
| } | |
| ; | |
| }; | |
| // Converts a count of time units since epoch, held as a BigInteger (the 96-bit FLBA(12) carrier), | |
| // into an Instant. Throws ArithmeticException if the seconds portion overflows long. Throws | |
| // DateTimeException if the combined seconds/nanos portion does not fit in an Instant. | |
| private static Instant instantFromUnits(BigInteger units, TimeUnit unit) { | |
| BigInteger[] secondsAndUnits = units.divideAndRemainder(BigInteger.valueOf(unit.convert(1, SECONDS))); | |
| long seconds = secondsAndUnits[0].longValueExact(); | |
| long nanos = secondsAndUnits[1].longValue() * unit.toNanos(1); | |
| return Instant.ofEpochSecond(seconds, nanos); | |
| } | |
| static final PrimitiveStringifier TIMESTAMP_MILLIS_STRINGIFIER = | |
| new TimestampStringifier("TIMESTAMP_MILLIS_STRINGIFIER", "yyyy-MM-dd'T'HH:mm:ss.SSS") { | |
| @Override | |
| Instant getInstant(long value) { | |
| return Instant.ofEpochMilli(value); | |
| } | |
| @Override | |
| Instant getInstant(BigInteger value) { | |
| return instantFromUnits(value, MILLISECONDS); | |
| } | |
| }; | |
| static final PrimitiveStringifier TIMESTAMP_MICROS_STRINGIFIER = | |
| new TimestampStringifier("TIMESTAMP_MICROS_STRINGIFIER", "yyyy-MM-dd'T'HH:mm:ss.SSSSSS") { | |
| @Override | |
| Instant getInstant(long value) { | |
| return Instant.ofEpochSecond( | |
| MICROSECONDS.toSeconds(value), MICROSECONDS.toNanos(value % SECONDS.toMicros(1))); | |
| } | |
| @Override | |
| Instant getInstant(BigInteger value) { | |
| return instantFromUnits(value, MICROSECONDS); | |
| } | |
| }; | |
| static final PrimitiveStringifier TIMESTAMP_NANOS_STRINGIFIER = | |
| new TimestampStringifier("TIMESTAMP_NANOS_STRINGIFIER", "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS") { | |
| @Override | |
| Instant getInstant(long value) { | |
| return Instant.ofEpochSecond( | |
| NANOSECONDS.toSeconds(value), NANOSECONDS.toNanos(value % SECONDS.toNanos(1))); | |
| } | |
| @Override | |
| Instant getInstant(BigInteger value) { | |
| return instantFromUnits(value, NANOSECONDS); | |
| } | |
| }; | |
| static final PrimitiveStringifier TIMESTAMP_MILLIS_UTC_STRINGIFIER = | |
| new TimestampStringifier("TIMESTAMP_MILLIS_UTC_STRINGIFIER", "yyyy-MM-dd'T'HH:mm:ss.SSSZ") { | |
| @Override | |
| Instant getInstant(long value) { | |
| return Instant.ofEpochMilli(value); | |
| } | |
| @Override | |
| Instant getInstant(BigInteger value) { | |
| return instantFromUnits(value, MILLISECONDS); | |
| } | |
| }; | |
| static final PrimitiveStringifier TIMESTAMP_MICROS_UTC_STRINGIFIER = | |
| new TimestampStringifier("TIMESTAMP_MICROS_UTC_STRINGIFIER", "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ") { | |
| @Override | |
| Instant getInstant(long value) { | |
| return Instant.ofEpochSecond( | |
| MICROSECONDS.toSeconds(value), MICROSECONDS.toNanos(value % SECONDS.toMicros(1))); | |
| } | |
| @Override | |
| Instant getInstant(BigInteger value) { | |
| return instantFromUnits(value, MICROSECONDS); | |
| } | |
| }; | |
| static final PrimitiveStringifier TIMESTAMP_NANOS_UTC_STRINGIFIER = | |
| new TimestampStringifier("TIMESTAMP_NANOS_UTC_STRINGIFIER", "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSSZ") { | |
| @Override | |
| Instant getInstant(long value) { | |
| return Instant.ofEpochSecond( | |
| NANOSECONDS.toSeconds(value), NANOSECONDS.toNanos(value % SECONDS.toNanos(1))); | |
| } | |
| @Override | |
| Instant getInstant(BigInteger value) { | |
| return instantFromUnits(value, NANOSECONDS); | |
| } | |
| }; | |
| private abstract static class TimeStringifier extends PrimitiveStringifier { | |
| private final boolean withZone; | |
| TimeStringifier(String name, boolean withZone) { | |
| super(name); | |
| this.withZone = withZone; | |
| } | |
| protected String toTimeString(long duration, TimeUnit unit) { | |
| String additionalFormat = (unit == MILLISECONDS ? "3d" : unit == MICROSECONDS ? "6d" : "9d"); | |
| String timeZone = withZone ? "+0000" : ""; | |
| String format = "%02d:%02d:%02d.%0" + additionalFormat + timeZone; | |
| return String.format( | |
| format, | |
| unit.toHours(duration), | |
| convert(duration, unit, MINUTES, HOURS), | |
| convert(duration, unit, SECONDS, MINUTES), | |
| convert(duration, unit, unit, SECONDS)); | |
| } | |
| protected long convert(long duration, TimeUnit from, TimeUnit to, TimeUnit higher) { | |
| return Math.abs(to.convert(duration, from) % to.convert(1, higher)); | |
| } | |
| } | |
| static final PrimitiveStringifier TIME_STRINGIFIER = new TimeStringifier("TIME_STRINGIFIER", false) { | |
| @Override | |
| public String stringify(int millis) { | |
| return toTimeString(millis, MILLISECONDS); | |
| } | |
| @Override | |
| public String stringify(long micros) { | |
| return toTimeString(micros, MICROSECONDS); | |
| } | |
| }; | |
| static final PrimitiveStringifier TIME_NANOS_STRINGIFIER = new TimeStringifier("TIME_NANOS_STRINGIFIER", false) { | |
| @Override | |
| public String stringify(long nanos) { | |
| return toTimeString(nanos, NANOSECONDS); | |
| } | |
| }; | |
| static final PrimitiveStringifier TIME_UTC_STRINGIFIER = new TimeStringifier("TIME_UTC_STRINGIFIER", true) { | |
| @Override | |
| public String stringify(int millis) { | |
| return toTimeString(millis, MILLISECONDS); | |
| } | |
| @Override | |
| public String stringify(long micros) { | |
| return toTimeString(micros, MICROSECONDS); | |
| } | |
| }; | |
| static final PrimitiveStringifier TIME_NANOS_UTC_STRINGIFIER = | |
| new TimeStringifier("TIME_NANOS_UTC_STRINGIFIER", true) { | |
| @Override | |
| public String stringify(long nanos) { | |
| return toTimeString(nanos, NANOSECONDS); | |
| } | |
| }; | |
| static PrimitiveStringifier createDecimalStringifier(final int scale) { | |
| return new BinaryStringifierBase("DECIMAL_STRINGIFIER(scale: " + scale + ")") { | |
| @Override | |
| public String stringify(int value) { | |
| return stringifyWithScale(BigInteger.valueOf(value)); | |
| } | |
| @Override | |
| public String stringify(long value) { | |
| return stringifyWithScale(BigInteger.valueOf(value)); | |
| } | |
| @Override | |
| String stringifyNotNull(Binary value) { | |
| try { | |
| return stringifyWithScale(new BigInteger(value.getBytesUnsafe())); | |
| } catch (NumberFormatException e) { | |
| return BINARY_INVALID; | |
| } | |
| } | |
| private String stringifyWithScale(BigInteger i) { | |
| return new BigDecimal(i, scale).toString(); | |
| } | |
| }; | |
| } | |
| static final PrimitiveStringifier UUID_STRINGIFIER = new PrimitiveStringifier("UUID_STRINGIFIER") { | |
| private final char[] digit = "0123456789abcdef".toCharArray(); | |
| @Override | |
| public String stringify(Binary value) { | |
| byte[] bytes = value.getBytesUnsafe(); | |
| StringBuilder builder = new StringBuilder(36); | |
| appendHex(bytes, 0, 4, builder); | |
| builder.append('-'); | |
| appendHex(bytes, 4, 2, builder); | |
| builder.append('-'); | |
| appendHex(bytes, 6, 2, builder); | |
| builder.append('-'); | |
| appendHex(bytes, 8, 2, builder); | |
| builder.append('-'); | |
| appendHex(bytes, 10, 6, builder); | |
| return builder.toString(); | |
| } | |
| private void appendHex(byte[] array, int offset, int length, StringBuilder builder) { | |
| for (int i = offset, n = offset + length; i < n; ++i) { | |
| int value = array[i] & 0xff; | |
| builder.append(digit[value >>> 4]).append(digit[value & 0x0f]); | |
| } | |
| } | |
| }; | |
| static final PrimitiveStringifier WKB_STRINGIFIER = new BinaryStringifierBase("WKB_STRINGIFIER") { | |
| @Override | |
| String stringifyNotNull(Binary value) { | |
| try { | |
| WKBReader reader = new WKBReader(); | |
| Geometry geometry = reader.read(value.getBytesUnsafe()); | |
| return geometry.toText(); | |
| } catch (ParseException e) { | |
| return BINARY_INVALID; | |
| } | |
| } | |
| }; | |
| static final PrimitiveStringifier FLOAT16_STRINGIFIER = new BinaryStringifierBase("FLOAT16_STRINGIFIER") { | |
| @Override | |
| String stringifyNotNull(Binary value) { | |
| return Float16.toFloatString(value); | |
| } | |
| }; | |
| static final PrimitiveStringifier UNKNOWN_STRINGIFIER = new PrimitiveStringifier("UNKNOWN_STRINGIFIER") { | |
| public String stringify(Binary ignored) { | |
| return "UNKNOWN"; | |
| } | |
| }; | |
| } |