/**
* 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");
}
}
}
}