/** * 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.avro; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class LogicalTypes { private static final Logger LOG = LoggerFactory.getLogger(LogicalTypes.class); public interface LogicalTypeFactory { LogicalType fromSchema(Schema schema); } private static final Map<String, LogicalTypeFactory> REGISTERED_TYPES = new ConcurrentHashMap<String, LogicalTypeFactory>(); public static void register(String logicalTypeName, LogicalTypeFactory factory) { if (logicalTypeName == null) { throw new NullPointerException("Invalid logical type name: null"); } if (factory == null) { throw new NullPointerException("Invalid logical type factory: null"); } REGISTERED_TYPES.put(logicalTypeName, factory); } /** * Returns the {@link LogicalType} from the schema, if one is present. */ public static LogicalType fromSchema(Schema schema) { return fromSchemaImpl(schema, true); } public static LogicalType fromSchemaIgnoreInvalid(Schema schema) { return fromSchemaImpl(schema, false); } private static LogicalType fromSchemaImpl(Schema schema, boolean throwErrors) { String typeName = schema.getProp(LogicalType.LOGICAL_TYPE_PROP); LogicalType logicalType; try { if (typeName == null) { logicalType = null; } else if (TIMESTAMP_MILLIS.equals(typeName)) { logicalType = TIMESTAMP_MILLIS_TYPE; } else if (DECIMAL.equals(typeName)) { logicalType = new Decimal(schema); } else if (UUID.equals(typeName)) { logicalType = UUID_TYPE; } else if (DATE.equals(typeName)) { logicalType = DATE_TYPE; } else if (TIMESTAMP_MICROS.equals(typeName)) { logicalType = TIMESTAMP_MICROS_TYPE; } else if (TIME_MILLIS.equals(typeName)) { logicalType = TIME_MILLIS_TYPE; } else if (TIME_MICROS.equals(typeName)) { logicalType = TIME_MICROS_TYPE; } else if (REGISTERED_TYPES.containsKey(typeName)) { logicalType = REGISTERED_TYPES.get(typeName).fromSchema(schema); } else { logicalType = null; } // make sure the type is valid before returning it if (logicalType != null) { logicalType.validate(schema); } } catch (RuntimeException e) { LOG.debug("Invalid logical type found", e); if (throwErrors) { throw e; } LOG.warn("Ignoring invalid logical type for name: {}", typeName); // ignore invalid types logicalType = null; } return logicalType; } private static final String DECIMAL = "decimal"; private static final String UUID = "uuid"; private static final String DATE = "date"; private static final String TIME_MILLIS = "time-millis"; private static final String TIME_MICROS = "time-micros"; private static final String TIMESTAMP_MILLIS = "timestamp-millis"; private static final String TIMESTAMP_MICROS = "timestamp-micros"; /** Create a Decimal LogicalType with the given precision and scale 0 */ public static Decimal decimal(int precision) { return decimal(precision, 0); } /** Create a Decimal LogicalType with the given precision and scale */ public static Decimal decimal(int precision, int scale) { return new Decimal(precision, scale); } private static final LogicalType UUID_TYPE = new LogicalType("uuid"); public static LogicalType uuid() { return UUID_TYPE; } private static final Date DATE_TYPE = new Date(); public static Date date() { return DATE_TYPE; } private static final TimeMillis TIME_MILLIS_TYPE = new TimeMillis(); public static TimeMillis timeMillis() { return TIME_MILLIS_TYPE; } private static final TimeMicros TIME_MICROS_TYPE = new TimeMicros(); public static TimeMicros timeMicros() { return TIME_MICROS_TYPE; } private static final TimestampMillis TIMESTAMP_MILLIS_TYPE = new TimestampMillis(); public static TimestampMillis timestampMillis() { return TIMESTAMP_MILLIS_TYPE; } private static final TimestampMicros TIMESTAMP_MICROS_TYPE = new TimestampMicros(); public static TimestampMicros timestampMicros() { return TIMESTAMP_MICROS_TYPE; } /** Decimal represents arbitrary-precision fixed-scale decimal numbers */ public static class Decimal extends LogicalType { private static final String PRECISION_PROP = "precision"; private static final String SCALE_PROP = "scale"; private final int precision; private final int scale; private Decimal(int precision, int scale) { super(DECIMAL); this.precision = precision; this.scale = scale; } private Decimal(Schema schema) { super("decimal"); if (!hasProperty(schema, PRECISION_PROP)) { throw new IllegalArgumentException( "Invalid decimal: missing precision"); } this.precision = getInt(schema, PRECISION_PROP); if (hasProperty(schema, SCALE_PROP)) { this.scale = getInt(schema, SCALE_PROP); } else { this.scale = 0; } } @Override public Schema addToSchema(Schema schema) { super.addToSchema(schema); schema.addProp(PRECISION_PROP, precision); schema.addProp(SCALE_PROP, scale); return schema; } public int getPrecision() { return precision; } public int getScale() { return scale; } @Override public void validate(Schema schema) { super.validate(schema); // validate the type if (schema.getType() != Schema.Type.FIXED && schema.getType() != Schema.Type.BYTES) { throw new IllegalArgumentException( "Logical type decimal must be backed by fixed or bytes"); } if (precision <= 0) { throw new IllegalArgumentException("Invalid decimal precision: " + precision + " (must be positive)"); } else if (precision > maxPrecision(schema)) { throw new IllegalArgumentException( "fixed(" + schema.getFixedSize() + ") cannot store " + precision + " digits (max " + maxPrecision(schema) + ")"); } if (scale < 0) { throw new IllegalArgumentException("Invalid decimal scale: " + scale + " (must be positive)"); } else if (scale > precision) { throw new IllegalArgumentException("Invalid decimal scale: " + scale + " (greater than precision: " + precision + ")"); } } private long maxPrecision(Schema schema) { if (schema.getType() == Schema.Type.BYTES) { // not bounded return Integer.MAX_VALUE; } else if (schema.getType() == Schema.Type.FIXED) { int size = schema.getFixedSize(); return Math.round( // convert double to long Math.floor(Math.log10( // number of base-10 digits Math.pow(2, 8 * size - 1) - 1) // max value stored )); } else { // not valid for any other type return 0; } } private boolean hasProperty(Schema schema, String name) { return (schema.getObjectProp(name) != null); } private int getInt(Schema schema, String name) { Object obj = schema.getObjectProp(name); if (obj instanceof Integer) { return (Integer) obj; } throw new IllegalArgumentException("Expected int " + name + ": " + (obj == null ? "null" : obj + ":" + obj.getClass().getSimpleName())); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Decimal decimal = (Decimal) o; if (precision != decimal.precision) return false; if (scale != decimal.scale) return false; return true; } @Override public int hashCode() { int result = precision; result = 31 * result + scale; return result; } } /** Date represents a date without a time */ public static class Date extends LogicalType { private Date() { super(DATE); } @Override public void validate(Schema schema) { super.validate(schema); if (schema.getType() != Schema.Type.INT) { throw new IllegalArgumentException( "Date can only be used with an underlying int type"); } } } /** TimeMillis represents a time in milliseconds without a date */ public static class TimeMillis extends LogicalType { private TimeMillis() { super(TIME_MILLIS); } @Override public void validate(Schema schema) { super.validate(schema); if (schema.getType() != Schema.Type.INT) { throw new IllegalArgumentException( "Time (millis) can only be used with an underlying int type"); } } } /** TimeMicros represents a time in microseconds without a date */ public static class TimeMicros extends LogicalType { private TimeMicros() { super(TIME_MICROS); } @Override public void validate(Schema schema) { super.validate(schema); if (schema.getType() != Schema.Type.LONG) { throw new IllegalArgumentException( "Time (micros) can only be used with an underlying long type"); } } } /** TimestampMillis represents a date and time in milliseconds */ public static class TimestampMillis extends LogicalType { private TimestampMillis() { super(TIMESTAMP_MILLIS); } @Override public void validate(Schema schema) { super.validate(schema); if (schema.getType() != Schema.Type.LONG) { throw new IllegalArgumentException( "Timestamp (millis) can only be used with an underlying long type"); } } } /** TimestampMicros represents a date and time in microseconds */ public static class TimestampMicros extends LogicalType { private TimestampMicros() { super(TIMESTAMP_MICROS); } @Override public void validate(Schema schema) { super.validate(schema); if (schema.getType() != Schema.Type.LONG) { throw new IllegalArgumentException( "Timestamp (micros) can only be used with an underlying long type"); } } } }