diff --git a/avro-java8/pom.xml b/avro-java8/pom.xml new file mode 100644 index 000000000..6afe15f0f --- /dev/null +++ b/avro-java8/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + com.fasterxml.jackson.dataformat + jackson-dataformats-binary + 2.10.0.pr2-SNAPSHOT + + jackson-dataformat-avro-java8 + Jackson dataformat: Avro Java 8 + bundle + Support for reading and writing AVRO-encoded data via Jackson +abstractions. + + http://github.com/FasterXML/jackson-dataformats-binary + + + + com/fasterxml/jackson/dataformat/avro/java8 + ${project.groupId}.avro.java8 + + + + + + com.fasterxml.jackson.core + jackson-annotations + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.dataformat + jackson-dataformat-avro + ${project.version} + + + + + ch.qos.logback + logback-classic + 1.1.3 + test + + + + org.projectlombok + lombok + 1.16.14 + test + + + + + org.assertj + assertj-core + 2.5.0 + test + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + + + + com.google.code.maven-replacer-plugin + replacer + + + process-packageVersion + generate-sources + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + + diff --git a/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/AvroJavaTimeAnnotationIntrospector.java b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/AvroJavaTimeAnnotationIntrospector.java new file mode 100644 index 000000000..ec0dd0532 --- /dev/null +++ b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/AvroJavaTimeAnnotationIntrospector.java @@ -0,0 +1,112 @@ +package com.fasterxml.jackson.dataformat.avro.java8; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.dataformat.avro.AvroAnnotationIntrospector; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import com.fasterxml.jackson.dataformat.avro.java8.deser.InstantDeserializer; +import com.fasterxml.jackson.dataformat.avro.java8.deser.LocalDateDeserializer; +import com.fasterxml.jackson.dataformat.avro.java8.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.dataformat.avro.java8.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.dataformat.avro.java8.ser.InstantSerializer; +import com.fasterxml.jackson.dataformat.avro.java8.ser.LocalDateSerializer; +import com.fasterxml.jackson.dataformat.avro.java8.ser.LocalDateTimeSerializer; +import com.fasterxml.jackson.dataformat.avro.java8.ser.LocalTimeSerializer; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +class AvroJavaTimeAnnotationIntrospector extends AvroAnnotationIntrospector { + static final AvroJavaTimeAnnotationIntrospector INSTANCE = new AvroJavaTimeAnnotationIntrospector(); + + @Override + public Object findSerializer(Annotated a) { + AvroType logicalType = _findAnnotation(a, AvroType.class); + if (null != logicalType) { + switch (logicalType.logicalType()) { + case TIMESTAMP_MILLISECOND: + if (a.getRawType().isAssignableFrom(LocalDateTime.class)) { + return LocalDateTimeSerializer.MILLIS; + } + if (a.getRawType().isAssignableFrom(Instant.class)) { + return InstantSerializer.MILLIS; + } + break; + case TIMESTAMP_MICROSECOND: + if (a.getRawType().isAssignableFrom(LocalDateTime.class)) { + return LocalDateTimeSerializer.MICROS; + } + if (a.getRawType().isAssignableFrom(Instant.class)) { + return InstantSerializer.MICROS; + } + break; + case DATE: + if (a.getRawType().isAssignableFrom(LocalDate.class)) { + return LocalDateSerializer.INSTANCE; + } + break; + case TIME_MILLISECOND: + if (a.getRawType().isAssignableFrom(LocalTime.class)) { + return LocalTimeSerializer.MILLIS; + } + break; + case TIME_MICROSECOND: + if (a.getRawType().isAssignableFrom(LocalTime.class)) { + return LocalTimeSerializer.MICROS; + } + break; + } + } + + return super.findSerializer(a); + } + + @Override + public Object findDeserializer(Annotated a) { + AvroType logicalType = _findAnnotation(a, AvroType.class); + if (null != logicalType) { + switch (logicalType.logicalType()) { + case TIMESTAMP_MILLISECOND: + if (a.getRawType().isAssignableFrom(LocalDateTime.class)) { + return LocalDateTimeDeserializer.MILLIS; + } + if (a.getRawType().isAssignableFrom(Instant.class)) { + return InstantDeserializer.MILLIS; + } + break; + case TIMESTAMP_MICROSECOND: + if (a.getRawType().isAssignableFrom(LocalDateTime.class)) { + return LocalDateTimeDeserializer.MICROS; + } + if (a.getRawType().isAssignableFrom(Instant.class)) { + return InstantDeserializer.MICROS; + } + break; + case DATE: + if (a.getRawType().isAssignableFrom(LocalDate.class)) { + return LocalDateDeserializer.INSTANCE; + } + break; + case TIME_MILLISECOND: + if (a.getRawType().isAssignableFrom(LocalTime.class)) { + return LocalTimeDeserializer.MILLIS; + } + break; + case TIME_MICROSECOND: + if (a.getRawType().isAssignableFrom(LocalTime.class)) { + return LocalTimeDeserializer.MICROS; + } + break; + } + } + + return super.findDeserializer(a); + } + + @Override + public Version version() { + return PackageVersion.VERSION; + } +} diff --git a/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/AvroJavaTimeModule.java b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/AvroJavaTimeModule.java new file mode 100644 index 000000000..977a21286 --- /dev/null +++ b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/AvroJavaTimeModule.java @@ -0,0 +1,20 @@ +package com.fasterxml.jackson.dataformat.avro.java8; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.dataformat.avro.AvroModule; + +public class AvroJavaTimeModule extends AvroModule { + + + public AvroJavaTimeModule() { + withAnnotationIntrospector(AvroJavaTimeAnnotationIntrospector.INSTANCE); + } + + + @Override + public Version version() { + return PackageVersion.VERSION; + } + + +} diff --git a/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/PackageVersion.java.in b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/PackageVersion.java.in new file mode 100644 index 000000000..7860aa14b --- /dev/null +++ b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/PackageVersion.java.in @@ -0,0 +1,20 @@ +package @package@; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.Versioned; +import com.fasterxml.jackson.core.util.VersionUtil; + +/** + * Automatically generated from PackageVersion.java.in during + * packageVersion-generate execution of maven-replacer-plugin in + * pom.xml. + */ +public final class PackageVersion implements Versioned { + public final static Version VERSION = VersionUtil.parseVersion( + "@projectversion@", "@projectgroupid@", "@projectartifactid@"); + + @Override + public Version version() { + return VERSION; + } +} diff --git a/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/deser/BaseTimeJsonDeserializer.java b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/deser/BaseTimeJsonDeserializer.java new file mode 100644 index 000000000..bb87ec8d0 --- /dev/null +++ b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/deser/BaseTimeJsonDeserializer.java @@ -0,0 +1,43 @@ +package com.fasterxml.jackson.dataformat.avro.java8.deser; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeUnit; + +public abstract class BaseTimeJsonDeserializer extends JsonDeserializer { + private final TimeUnit resolution; + final ZoneId zoneId = ZoneId.of("UTC"); + + BaseTimeJsonDeserializer(TimeUnit resolution) { + this.resolution = resolution; + } + + abstract T fromInstant(Instant input); + + @Override + public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + final long input = p.getLongValue(); + final ChronoUnit chronoUnit; + + switch (this.resolution) { + case MICROSECONDS: + chronoUnit = ChronoUnit.MICROS; + break; + case MILLISECONDS: + chronoUnit = ChronoUnit.MILLIS; + break; + default: + throw new UnsupportedOperationException( + String.format("%s is not supported", this.resolution) + ); + } + final Instant instant = Instant.EPOCH.plus(input, chronoUnit); + return fromInstant(instant); + } +} diff --git a/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/deser/InstantDeserializer.java b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/deser/InstantDeserializer.java new file mode 100644 index 000000000..73285874e --- /dev/null +++ b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/deser/InstantDeserializer.java @@ -0,0 +1,20 @@ +package com.fasterxml.jackson.dataformat.avro.java8.deser; + +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.time.Instant; +import java.util.concurrent.TimeUnit; + +public class InstantDeserializer extends BaseTimeJsonDeserializer { + public static JsonDeserializer MILLIS = new InstantDeserializer(TimeUnit.MILLISECONDS); + public static JsonDeserializer MICROS = new InstantDeserializer(TimeUnit.MICROSECONDS); + + InstantDeserializer(TimeUnit resolution) { + super(resolution); + } + + @Override + Instant fromInstant(Instant input) { + return input; + } +} diff --git a/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/deser/LocalDateDeserializer.java b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/deser/LocalDateDeserializer.java new file mode 100644 index 000000000..41b321b25 --- /dev/null +++ b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/deser/LocalDateDeserializer.java @@ -0,0 +1,18 @@ +package com.fasterxml.jackson.dataformat.avro.java8.deser; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.time.LocalDate; + +public class LocalDateDeserializer extends JsonDeserializer { + public static final JsonDeserializer INSTANCE = new LocalDateDeserializer(); + + @Override + public LocalDate deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + return LocalDate.ofEpochDay(jsonParser.getLongValue()); + } +} diff --git a/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/deser/LocalDateTimeDeserializer.java b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/deser/LocalDateTimeDeserializer.java new file mode 100644 index 000000000..b63790dc9 --- /dev/null +++ b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/deser/LocalDateTimeDeserializer.java @@ -0,0 +1,21 @@ +package com.fasterxml.jackson.dataformat.avro.java8.deser; + +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +public class LocalDateTimeDeserializer extends BaseTimeJsonDeserializer { + public static JsonDeserializer MILLIS = new LocalDateTimeDeserializer(TimeUnit.MILLISECONDS); + public static JsonDeserializer MICROS = new LocalDateTimeDeserializer(TimeUnit.MICROSECONDS); + + LocalDateTimeDeserializer(TimeUnit resolution) { + super(resolution); + } + + @Override + protected LocalDateTime fromInstant(Instant input) { + return LocalDateTime.ofInstant(input, this.zoneId); + } +} diff --git a/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/deser/LocalTimeDeserializer.java b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/deser/LocalTimeDeserializer.java new file mode 100644 index 000000000..0d2a24f2b --- /dev/null +++ b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/deser/LocalTimeDeserializer.java @@ -0,0 +1,28 @@ +package com.fasterxml.jackson.dataformat.avro.java8.deser; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.time.LocalTime; +import java.util.concurrent.TimeUnit; + +public class LocalTimeDeserializer extends JsonDeserializer { + public static JsonDeserializer MILLIS = new LocalTimeDeserializer(TimeUnit.MILLISECONDS); + public static JsonDeserializer MICROS = new LocalTimeDeserializer(TimeUnit.MICROSECONDS); + + final TimeUnit resolution; + + LocalTimeDeserializer(TimeUnit resolution) { + this.resolution = resolution; + } + + @Override + public LocalTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + long value = jsonParser.getLongValue(); + long nanos = this.resolution.toNanos(value); + return LocalTime.ofNanoOfDay(nanos); + } +} diff --git a/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/deser/OffsetDateTimeDeserializer.java b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/deser/OffsetDateTimeDeserializer.java new file mode 100644 index 000000000..80b472b0c --- /dev/null +++ b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/deser/OffsetDateTimeDeserializer.java @@ -0,0 +1,21 @@ +package com.fasterxml.jackson.dataformat.avro.java8.deser; + +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.util.concurrent.TimeUnit; + +public class OffsetDateTimeDeserializer extends BaseTimeJsonDeserializer { + public static JsonDeserializer MILLIS = new OffsetDateTimeDeserializer(TimeUnit.MILLISECONDS); + public static JsonDeserializer MICROS = new OffsetDateTimeDeserializer(TimeUnit.MICROSECONDS); + + OffsetDateTimeDeserializer(TimeUnit resolution) { + super(resolution); + } + + @Override + protected OffsetDateTime fromInstant(Instant input) { + return OffsetDateTime.ofInstant(input, this.zoneId); + } +} diff --git a/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/deser/ZonedDateTimeDeserializer.java b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/deser/ZonedDateTimeDeserializer.java new file mode 100644 index 000000000..c1ae6eae6 --- /dev/null +++ b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/deser/ZonedDateTimeDeserializer.java @@ -0,0 +1,21 @@ +package com.fasterxml.jackson.dataformat.avro.java8.deser; + +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.concurrent.TimeUnit; + +public class ZonedDateTimeDeserializer extends BaseTimeJsonDeserializer { + public static JsonDeserializer MILLIS = new ZonedDateTimeDeserializer(TimeUnit.MILLISECONDS); + public static JsonDeserializer MICROS = new ZonedDateTimeDeserializer(TimeUnit.MICROSECONDS); + + ZonedDateTimeDeserializer(TimeUnit resolution) { + super(resolution); + } + + @Override + protected ZonedDateTime fromInstant(Instant input) { + return ZonedDateTime.ofInstant(input, this.zoneId); + } +} diff --git a/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/ser/BaseTimeJsonSerializer.java b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/ser/BaseTimeJsonSerializer.java new file mode 100644 index 000000000..d388fa523 --- /dev/null +++ b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/ser/BaseTimeJsonSerializer.java @@ -0,0 +1,41 @@ +package com.fasterxml.jackson.dataformat.avro.java8.ser; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeUnit; + +public abstract class BaseTimeJsonSerializer extends JsonSerializer { + final TimeUnit resolution; + final ZoneId zoneId = ZoneId.of("UTC"); + + BaseTimeJsonSerializer(TimeUnit resolution) { + this.resolution = resolution; + } + + abstract Instant toInstant(T input); + + @Override + public void serialize(T input, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + final Instant instant = toInstant(input); + final long output; + switch (this.resolution) { + case MICROSECONDS: + output = ChronoUnit.MICROS.between(Instant.EPOCH, instant); + break; + case MILLISECONDS: + output = instant.toEpochMilli(); + break; + default: + throw new UnsupportedOperationException( + String.format("%s is not supported", this.resolution) + ); + } + jsonGenerator.writeNumber(output); + } +} diff --git a/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/ser/InstantSerializer.java b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/ser/InstantSerializer.java new file mode 100644 index 000000000..eba3fc089 --- /dev/null +++ b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/ser/InstantSerializer.java @@ -0,0 +1,20 @@ +package com.fasterxml.jackson.dataformat.avro.java8.ser; + +import com.fasterxml.jackson.databind.JsonSerializer; + +import java.time.Instant; +import java.util.concurrent.TimeUnit; + +public class InstantSerializer extends BaseTimeJsonSerializer { + public static final JsonSerializer MILLIS = new InstantSerializer(TimeUnit.MILLISECONDS); + public static final JsonSerializer MICROS = new InstantSerializer(TimeUnit.MICROSECONDS); + + InstantSerializer(TimeUnit resolution) { + super(resolution); + } + + @Override + Instant toInstant(Instant input) { + return input; + } +} diff --git a/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/ser/LocalDateSerializer.java b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/ser/LocalDateSerializer.java new file mode 100644 index 000000000..7a37aedfd --- /dev/null +++ b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/ser/LocalDateSerializer.java @@ -0,0 +1,17 @@ +package com.fasterxml.jackson.dataformat.avro.java8.ser; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.time.LocalDate; + +public class LocalDateSerializer extends JsonSerializer { + public static final JsonSerializer INSTANCE = new LocalDateSerializer(); + + @Override + public void serialize(LocalDate localDate, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeNumber(localDate.toEpochDay()); + } +} diff --git a/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/ser/LocalDateTimeSerializer.java b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/ser/LocalDateTimeSerializer.java new file mode 100644 index 000000000..f40c3d6df --- /dev/null +++ b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/ser/LocalDateTimeSerializer.java @@ -0,0 +1,22 @@ +package com.fasterxml.jackson.dataformat.avro.java8.ser; + +import com.fasterxml.jackson.databind.JsonSerializer; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.concurrent.TimeUnit; + +public class LocalDateTimeSerializer extends BaseTimeJsonSerializer { + public static final JsonSerializer MILLIS = new LocalDateTimeSerializer(TimeUnit.MILLISECONDS); + public static final JsonSerializer MICROS = new LocalDateTimeSerializer(TimeUnit.MICROSECONDS); + + LocalDateTimeSerializer(TimeUnit resolution) { + super(resolution); + } + + @Override + Instant toInstant(LocalDateTime input) { + return input.toInstant(ZoneOffset.UTC); + } +} diff --git a/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/ser/LocalTimeSerializer.java b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/ser/LocalTimeSerializer.java new file mode 100644 index 000000000..eb6268765 --- /dev/null +++ b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/ser/LocalTimeSerializer.java @@ -0,0 +1,38 @@ +package com.fasterxml.jackson.dataformat.avro.java8.ser; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.time.LocalTime; +import java.util.concurrent.TimeUnit; + +public class LocalTimeSerializer extends JsonSerializer { + public static final JsonSerializer MILLIS = new LocalTimeSerializer(TimeUnit.MILLISECONDS); + public static final JsonSerializer MICROS = new LocalTimeSerializer(TimeUnit.MICROSECONDS); + + private final TimeUnit resolution; + + LocalTimeSerializer(TimeUnit resolution) { + this.resolution = resolution; + } + + @Override + public void serialize(LocalTime localTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + switch (this.resolution) { + case MICROSECONDS: + long micros = TimeUnit.NANOSECONDS.toMicros(localTime.toNanoOfDay()); + jsonGenerator.writeNumber(micros); + break; + case MILLISECONDS: + int millis = (int)TimeUnit.NANOSECONDS.toMillis(localTime.toNanoOfDay()); + jsonGenerator.writeNumber(millis); + break; + default: + throw new UnsupportedOperationException( + String.format("%s is not supported", this.resolution) + ); + } + } +} diff --git a/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/ser/OffsetDateTimeSerializer.java b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/ser/OffsetDateTimeSerializer.java new file mode 100644 index 000000000..65f14147b --- /dev/null +++ b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/ser/OffsetDateTimeSerializer.java @@ -0,0 +1,21 @@ +package com.fasterxml.jackson.dataformat.avro.java8.ser; + +import com.fasterxml.jackson.databind.JsonSerializer; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.util.concurrent.TimeUnit; + +public class OffsetDateTimeSerializer extends BaseTimeJsonSerializer { + public static final JsonSerializer MILLIS = new OffsetDateTimeSerializer(TimeUnit.MILLISECONDS); + public static final JsonSerializer MICROS = new OffsetDateTimeSerializer(TimeUnit.MICROSECONDS); + + OffsetDateTimeSerializer(TimeUnit resolution) { + super(resolution); + } + + @Override + Instant toInstant(OffsetDateTime input) { + return input.toInstant(); + } +} diff --git a/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/ser/ZonedDateTimeSerializer.java b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/ser/ZonedDateTimeSerializer.java new file mode 100644 index 000000000..7cc61b697 --- /dev/null +++ b/avro-java8/src/main/java/com/fasterxml/jackson/dataformat/avro/java8/ser/ZonedDateTimeSerializer.java @@ -0,0 +1,21 @@ +package com.fasterxml.jackson.dataformat.avro.java8.ser; + +import com.fasterxml.jackson.databind.JsonSerializer; + +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.concurrent.TimeUnit; + +public class ZonedDateTimeSerializer extends BaseTimeJsonSerializer { + public static final JsonSerializer MILLIS = new ZonedDateTimeSerializer(TimeUnit.MILLISECONDS); + public static final JsonSerializer MICROS = new ZonedDateTimeSerializer(TimeUnit.MICROSECONDS); + + ZonedDateTimeSerializer(TimeUnit resolution) { + super(resolution); + } + + @Override + Instant toInstant(ZonedDateTime input) { + return input.toInstant(); + } +} diff --git a/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/BytesDecimalTest.java b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/BytesDecimalTest.java new file mode 100644 index 000000000..a9a92fca1 --- /dev/null +++ b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/BytesDecimalTest.java @@ -0,0 +1,50 @@ +package com.fasterxml.jackson.dataformat.avro.java8.logicaltypes; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import org.apache.avro.Conversions; +import org.apache.avro.Schema; + +import java.math.BigDecimal; + +public class BytesDecimalTest extends LogicalTypeTestCase { + static final BigDecimal VALUE = BigDecimal.valueOf(123456, 3); + + @Override + protected Class dataClass() { + return BytesDecimal.class; + } + + @Override + protected Schema.Type schemaType() { + return Schema.Type.BYTES; + } + + @Override + protected String logicalType() { + return "decimal"; + } + + @Override + protected BytesDecimal testData() { + BytesDecimal v = new BytesDecimal(); + v.value = VALUE; + return v; + } + + @Override + protected Object convertedValue() { + return new Conversions.DecimalConversion().toBytes(VALUE, this.schema, this.schema.getLogicalType()); + } + + static class BytesDecimal extends TestData { + @JsonProperty(required = true) + @AvroType(precision = 3, scale = 3, schemaType = Schema.Type.BYTES, logicalType = AvroType.LogicalType.DECIMAL) + public BigDecimal value; + + @Override + public BigDecimal value() { + return this.value; + } + } +} diff --git a/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/FixedDecimalTest.java b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/FixedDecimalTest.java new file mode 100644 index 000000000..a8c23c926 --- /dev/null +++ b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/FixedDecimalTest.java @@ -0,0 +1,51 @@ +package com.fasterxml.jackson.dataformat.avro.java8.logicaltypes; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import org.apache.avro.Conversions; +import org.apache.avro.Schema; + +import java.math.BigDecimal; + +public class FixedDecimalTest extends LogicalTypeTestCase { + static final BigDecimal VALUE = BigDecimal.valueOf(123456, 3); + + @Override + protected Class dataClass() { + return FixedDecimal.class; + } + + @Override + protected Schema.Type schemaType() { + return Schema.Type.FIXED; + } + + @Override + protected String logicalType() { + return "decimal"; + } + + @Override + protected FixedDecimal testData() { + FixedDecimal v = new FixedDecimal(); + v.value = VALUE; + return v; + } + + @Override + protected Object convertedValue() { + return new Conversions.DecimalConversion().toFixed(VALUE, this.schema, this.schema.getLogicalType()); + } + + static class FixedDecimal extends TestData { + @JsonProperty(required = true) + @AvroType(precision = 3, scale = 3, fixedSize = 8, typeNamespace = "com.foo.example", typeName = "Decimal", schemaType = Schema.Type.FIXED, logicalType = AvroType.LogicalType.DECIMAL) + public BigDecimal value; + + @Override + public BigDecimal value() { + return this.value; + } + } + +} diff --git a/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/LogicalTypeTestCase.java b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/LogicalTypeTestCase.java new file mode 100644 index 000000000..e6475a6bf --- /dev/null +++ b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/LogicalTypeTestCase.java @@ -0,0 +1,101 @@ +package com.fasterxml.jackson.dataformat.avro.java8.logicaltypes; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.dataformat.avro.AvroMapper; +import com.fasterxml.jackson.dataformat.avro.AvroSchema; +import com.fasterxml.jackson.dataformat.avro.java8.AvroJavaTimeModule; +import junit.framework.TestCase; +import org.apache.avro.Schema; +import org.apache.avro.generic.GenericData; +import org.apache.avro.generic.GenericDatumWriter; +import org.apache.avro.io.BinaryEncoder; +import org.apache.avro.io.DatumWriter; +import org.apache.avro.io.EncoderFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; + +public abstract class LogicalTypeTestCase extends TestCase { + protected AvroMapper mapper; + protected AvroSchema avroSchema; + protected Schema recordSchema; + protected Schema schema; + protected Class dataClass; + protected Schema.Type schemaType; + protected String logicalType; + + protected abstract Class dataClass(); + + protected abstract Schema.Type schemaType(); + + protected abstract String logicalType(); + + protected abstract T testData(); + + protected abstract Object convertedValue(); + + + @Override + protected void setUp() throws Exception { + this.mapper = new AvroMapper(new AvroJavaTimeModule()); + this.dataClass = dataClass(); + + this.avroSchema = mapper.schemaFor(this.dataClass); + assertNotNull("AvroSchema should not be null", this.avroSchema); + this.recordSchema = this.avroSchema.getAvroSchema(); + assertNotNull("Schema should not be null", this.recordSchema); + assertEquals("Schema should be a record.", Schema.Type.RECORD, this.recordSchema.getType()); + Schema.Field field = this.recordSchema.getField("value"); + assertNotNull("schema must have a 'value' field", field); + this.schema = field.schema(); + this.schemaType = schemaType(); + this.logicalType = logicalType(); + + System.out.println(recordSchema.toString(true)); + configure(this.mapper); + } + + protected void configure(AvroMapper mapper) { + + } + + public void testSchemaType() { + assertEquals("schema.getType() does not match.", this.schemaType, this.schema.getType()); + } + + public void testLogicalType() { + assertNotNull("schema.getLogicalType() should not return null", this.schema.getLogicalType()); + assertEquals("schema.getLogicalType().getName() does not match.", this.logicalType, this.schema.getLogicalType().getName()); + } + + byte[] serialize(T expected) throws JsonProcessingException { + final byte[] actualbytes = this.mapper.writer(this.avroSchema).writeValueAsBytes(expected); + return actualbytes; + } + + public void testRoundTrip() throws IOException { + final T expected = testData(); + final byte[] actualbytes = serialize(expected); + final T actual = this.mapper.reader(avroSchema).forType(this.dataClass).readValue(actualbytes); + assertNotNull("actual should not be null.", actual); + assertEquals(expected.value(), actual.value()); + } + + public void testAvroSerialization() throws IOException { + final T expected = testData(); + final byte[] actualbytes = serialize(expected); + final Object convertedValue = convertedValue(); + byte[] expectedBytes; + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + BinaryEncoder encoder = EncoderFactory.get().directBinaryEncoder(outputStream, null); + GenericData.Record record = new GenericData.Record(this.recordSchema); + record.put("value", convertedValue); + DatumWriter writer = new GenericDatumWriter(this.recordSchema); + writer.write(record, encoder); + expectedBytes = outputStream.toByteArray(); + } + + assertTrue("serialized output does not match avro version.", Arrays.equals(expectedBytes, actualbytes)); + } +} diff --git a/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/TestData.java b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/TestData.java new file mode 100644 index 000000000..c01309015 --- /dev/null +++ b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/TestData.java @@ -0,0 +1,5 @@ +package com.fasterxml.jackson.dataformat.avro.java8.logicaltypes; + +public abstract class TestData { + public abstract T value(); +} diff --git a/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/UUIDTest.java b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/UUIDTest.java new file mode 100644 index 000000000..6a54b0d90 --- /dev/null +++ b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/UUIDTest.java @@ -0,0 +1,49 @@ +package com.fasterxml.jackson.dataformat.avro.java8.logicaltypes; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import org.apache.avro.Schema; + +import java.util.UUID; + +public class UUIDTest extends LogicalTypeTestCase { + static final UUID VALUE = UUID.randomUUID(); + + @Override + protected Class dataClass() { + return UUIDTestCase.class; + } + + @Override + protected Schema.Type schemaType() { + return Schema.Type.STRING; + } + + @Override + protected String logicalType() { + return "uuid"; + } + + @Override + protected UUIDTestCase testData() { + UUIDTestCase v = new UUIDTestCase(); + v.value = VALUE; + return v; + } + + @Override + protected Object convertedValue() { + return VALUE.toString(); + } + + static class UUIDTestCase extends TestData { + @JsonProperty(required = true) + @AvroType(schemaType = Schema.Type.STRING, logicalType = AvroType.LogicalType.UUID) + public UUID value; + + @Override + public UUID value() { + return this.value; + } + } +} diff --git a/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/DateDateTest.java b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/DateDateTest.java new file mode 100644 index 000000000..a490a65d0 --- /dev/null +++ b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/DateDateTest.java @@ -0,0 +1,60 @@ +package com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.time; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import com.fasterxml.jackson.dataformat.avro.AvroMapper; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.LogicalTypeTestCase; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.TestData; +import org.apache.avro.Schema; + +import java.time.LocalDate; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +public class DateDateTest extends LogicalTypeTestCase { + static final LocalDate VALUE = LocalDate.of(2011, 3, 14); + + @Override + protected Class dataClass() { + return TestCase.class; + } + + @Override + protected Schema.Type schemaType() { + return Schema.Type.INT; + } + + @Override + protected String logicalType() { + return "date"; + } + + @Override + protected TestCase testData() { + TestCase v = new TestCase(); + v.value = new Date(TimeUnit.DAYS.toMillis(VALUE.toEpochDay())); + return v; + } + + @Override + protected Object convertedValue() { + return VALUE.toEpochDay(); + } + + @Override + protected void configure(AvroMapper mapper) { + + } + + static class TestCase extends TestData { + @JsonProperty(required = true) + @AvroType(schemaType = Schema.Type.INT, logicalType = AvroType.LogicalType.DATE) + Date value; + + @Override + public Date value() { + return this.value; + } + } + +} diff --git a/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/DateLocalDateTest.java b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/DateLocalDateTest.java new file mode 100644 index 000000000..a0079c368 --- /dev/null +++ b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/DateLocalDateTest.java @@ -0,0 +1,58 @@ +package com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.time; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import com.fasterxml.jackson.dataformat.avro.AvroMapper; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.LogicalTypeTestCase; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.TestData; +import org.apache.avro.Schema; + +import java.time.LocalDate; + +public class DateLocalDateTest extends LogicalTypeTestCase { + static final LocalDate VALUE = LocalDate.of(2011, 3, 14); + + @Override + protected Class dataClass() { + return TestCase.class; + } + + @Override + protected Schema.Type schemaType() { + return Schema.Type.INT; + } + + @Override + protected String logicalType() { + return "date"; + } + + @Override + protected TestCase testData() { + TestCase v = new TestCase(); + v.value = VALUE; + return v; + } + + @Override + protected Object convertedValue() { + return VALUE.toEpochDay(); + } + + @Override + protected void configure(AvroMapper mapper) { + + } + + static class TestCase extends TestData { + @JsonProperty(required = true) + @AvroType(schemaType = Schema.Type.INT, logicalType = AvroType.LogicalType.DATE) + LocalDate value; + + @Override + public LocalDate value() { + return this.value; + } + } + +} diff --git a/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimeMicrosDateTest.java b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimeMicrosDateTest.java new file mode 100644 index 000000000..a5cb4061f --- /dev/null +++ b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimeMicrosDateTest.java @@ -0,0 +1,62 @@ +package com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.time; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import com.fasterxml.jackson.dataformat.avro.AvroMapper; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.LogicalTypeTestCase; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.TestData; +import org.apache.avro.Schema; + +import java.time.LocalTime; +import java.time.ZoneOffset; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +public class TimeMicrosDateTest extends LogicalTypeTestCase { + static final Date VALUE = new Date(8127123L); + + @Override + protected Class dataClass() { + return TestCase.class; + } + + @Override + protected Schema.Type schemaType() { + return Schema.Type.INT; + } + + @Override + protected String logicalType() { + return "time-millis"; + } + + @Override + protected TestCase testData() { + TestCase v = new TestCase(); + v.value = VALUE; + return v; + } + + @Override + protected Object convertedValue() { + LocalTime time = VALUE.toInstant().atOffset(ZoneOffset.UTC).toLocalTime(); + return TimeUnit.NANOSECONDS.toMillis(time.toNanoOfDay()); + } + + @Override + protected void configure(AvroMapper mapper) { + + } + + static class TestCase extends TestData { + @JsonProperty(required = true) + @AvroType(schemaType = Schema.Type.INT, logicalType = AvroType.LogicalType.TIME_MILLISECOND) + Date value; + + @Override + public Date value() { + return this.value; + } + } + +} diff --git a/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimeMicrosLocalTimeTest.java b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimeMicrosLocalTimeTest.java new file mode 100644 index 000000000..92a2cb4bf --- /dev/null +++ b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimeMicrosLocalTimeTest.java @@ -0,0 +1,59 @@ +package com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.time; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import com.fasterxml.jackson.dataformat.avro.AvroMapper; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.LogicalTypeTestCase; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.TestData; +import org.apache.avro.Schema; + +import java.time.LocalTime; +import java.util.concurrent.TimeUnit; + +public class TimeMicrosLocalTimeTest extends LogicalTypeTestCase { + static final LocalTime VALUE = LocalTime.of(3, 3, 14); + + @Override + protected Class dataClass() { + return TestCase.class; + } + + @Override + protected Schema.Type schemaType() { + return Schema.Type.LONG; + } + + @Override + protected String logicalType() { + return "time-micros"; + } + + @Override + protected TestCase testData() { + TestCase v = new TestCase(); + v.value = VALUE; + return v; + } + + @Override + protected Object convertedValue() { + return TimeUnit.NANOSECONDS.toMicros(VALUE.toNanoOfDay()); + } + + @Override + protected void configure(AvroMapper mapper) { + + } + + static class TestCase extends TestData { + @JsonProperty(required = true) + @AvroType(schemaType = Schema.Type.LONG, logicalType = AvroType.LogicalType.TIME_MICROSECOND) + LocalTime value; + + @Override + public LocalTime value() { + return this.value; + } + } + +} diff --git a/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimeMillisDateTest.java b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimeMillisDateTest.java new file mode 100644 index 000000000..db22f1419 --- /dev/null +++ b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimeMillisDateTest.java @@ -0,0 +1,63 @@ +package com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.time; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import com.fasterxml.jackson.dataformat.avro.AvroMapper; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.LogicalTypeTestCase; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.TestData; +import org.apache.avro.Schema; + +import java.time.LocalTime; +import java.time.ZoneOffset; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +public class TimeMillisDateTest extends LogicalTypeTestCase { + static final Date VALUE = new Date(8127123L); + + @Override + protected Class dataClass() { + return TestCase.class; + } + + @Override + protected Schema.Type schemaType() { + return Schema.Type.INT; + } + + @Override + protected String logicalType() { + return "time-millis"; + } + + @Override + protected TestCase testData() { + TestCase v = new TestCase(); + v.value = VALUE; + return v; + } + + @Override + protected Object convertedValue() { + LocalTime time = VALUE.toInstant().atOffset(ZoneOffset.UTC).toLocalTime(); + return TimeUnit.NANOSECONDS.toMillis(time.toNanoOfDay()); + } + + @Override + protected void configure(AvroMapper mapper) { + + } + + static class TestCase extends TestData { + @JsonProperty(required = true) + @AvroType(schemaType = Schema.Type.INT, logicalType = AvroType.LogicalType.TIME_MILLISECOND) + + Date value; + + @Override + public Date value() { + return this.value; + } + } + +} diff --git a/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimeMillisLocalTimeTest.java b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimeMillisLocalTimeTest.java new file mode 100644 index 000000000..ec5fd4ac8 --- /dev/null +++ b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimeMillisLocalTimeTest.java @@ -0,0 +1,59 @@ +package com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.time; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import com.fasterxml.jackson.dataformat.avro.AvroMapper; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.LogicalTypeTestCase; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.TestData; +import org.apache.avro.Schema; + +import java.time.LocalTime; +import java.util.concurrent.TimeUnit; + +public class TimeMillisLocalTimeTest extends LogicalTypeTestCase { + static final LocalTime VALUE = LocalTime.of(3, 3, 14); + + @Override + protected Class dataClass() { + return TestCase.class; + } + + @Override + protected Schema.Type schemaType() { + return Schema.Type.INT; + } + + @Override + protected String logicalType() { + return "time-millis"; + } + + @Override + protected TestCase testData() { + TestCase v = new TestCase(); + v.value = VALUE; + return v; + } + + @Override + protected Object convertedValue() { + return TimeUnit.NANOSECONDS.toMillis(VALUE.toNanoOfDay()); + } + + @Override + protected void configure(AvroMapper mapper) { + + } + + static class TestCase extends TestData { + @JsonProperty(required = true) + @AvroType(schemaType = Schema.Type.INT, logicalType = AvroType.LogicalType.TIME_MILLISECOND) + LocalTime value; + + @Override + public LocalTime value() { + return this.value; + } + } + +} diff --git a/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMicrosDateTest.java b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMicrosDateTest.java new file mode 100644 index 000000000..c94f9aa25 --- /dev/null +++ b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMicrosDateTest.java @@ -0,0 +1,53 @@ +package com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.time; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.LogicalTypeTestCase; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.TestData; +import org.apache.avro.Schema; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +public class TimestampMicrosDateTest extends LogicalTypeTestCase { + static final Date VALUE = new Date(1526955327123L); + + @Override + protected Class dataClass() { + return TestCase.class; + } + + @Override + protected Schema.Type schemaType() { + return Schema.Type.LONG; + } + + @Override + protected String logicalType() { + return "timestamp-micros"; + } + + @Override + protected TestCase testData() { + TestCase v = new TestCase(); + v.value = VALUE; + return v; + } + + @Override + protected Object convertedValue() { + return TimeUnit.MILLISECONDS.toMicros(VALUE.getTime()); + } + + static class TestCase extends TestData { + @JsonProperty(required = true) + @AvroType(schemaType = Schema.Type.LONG, logicalType = AvroType.LogicalType.TIMESTAMP_MICROSECOND) + Date value; + + @Override + public Date value() { + return this.value; + } + } + +} diff --git a/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMicrosInstantTest.java b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMicrosInstantTest.java new file mode 100644 index 000000000..83aff00ac --- /dev/null +++ b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMicrosInstantTest.java @@ -0,0 +1,55 @@ +package com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.time; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.LogicalTypeTestCase; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.TestData; +import org.apache.avro.Schema; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +public class TimestampMicrosInstantTest extends LogicalTypeTestCase { + static final Instant VALUE = new Date(1526955327123L).toInstant(); + + @Override + protected Class dataClass() { + return TestCase.class; + } + + @Override + protected Schema.Type schemaType() { + return Schema.Type.LONG; + } + + @Override + protected String logicalType() { + return "timestamp-micros"; + } + + @Override + protected TestCase testData() { + TestCase v = new TestCase(); + v.value = VALUE; + return v; + } + + @Override + protected Object convertedValue() { + return ChronoUnit.MICROS.between(Instant.EPOCH, VALUE); + } + + static class TestCase extends TestData { + @JsonProperty(required = true) + @AvroType(schemaType = Schema.Type.LONG, logicalType = AvroType.LogicalType.TIMESTAMP_MICROSECOND) + Instant value; + + @Override + public Instant value() { + return this.value; + } + } + +} diff --git a/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMicrosLocalDateTimeTest.java b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMicrosLocalDateTimeTest.java new file mode 100644 index 000000000..bdd4dd54d --- /dev/null +++ b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMicrosLocalDateTimeTest.java @@ -0,0 +1,67 @@ +package com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.time; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import com.fasterxml.jackson.dataformat.avro.AvroMapper; +import com.fasterxml.jackson.dataformat.avro.java8.AvroJavaTimeModule; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.LogicalTypeTestCase; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.TestData; +import org.apache.avro.Schema; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.concurrent.TimeUnit; + +public class TimestampMicrosLocalDateTimeTest extends LogicalTypeTestCase { + static final LocalDateTime VALUE = LocalDateTime.ofInstant( + Instant.ofEpochMilli(1526955327123L), + ZoneId.of("UTC") + ); + + @Override + protected Class dataClass() { + return TestCase.class; + } + + @Override + protected Schema.Type schemaType() { + return Schema.Type.LONG; + } + + @Override + protected String logicalType() { + return "timestamp-micros"; + } + + @Override + protected TestCase testData() { + TestCase v = new TestCase(); + v.value = VALUE; + return v; + } + + @Override + protected Object convertedValue() { + Instant instant = VALUE.toInstant(ZoneOffset.UTC); + return (TimeUnit.SECONDS.toMicros(instant.getEpochSecond()) + TimeUnit.NANOSECONDS.toMicros(instant.getNano())); + } + + @Override + protected void configure(AvroMapper mapper) { + mapper.registerModule(new AvroJavaTimeModule()); + } + + static class TestCase extends TestData { + @JsonProperty(required = true) + @AvroType(schemaType = Schema.Type.LONG, logicalType = AvroType.LogicalType.TIMESTAMP_MICROSECOND) + LocalDateTime value; + + @Override + public LocalDateTime value() { + return this.value; + } + } + +} diff --git a/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMicrosOffsetDateTimeTest.java b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMicrosOffsetDateTimeTest.java new file mode 100644 index 000000000..8910a743c --- /dev/null +++ b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMicrosOffsetDateTimeTest.java @@ -0,0 +1,58 @@ +package com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.time; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.LogicalTypeTestCase; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.TestData; +import org.apache.avro.Schema; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; + +public class TimestampMicrosOffsetDateTimeTest extends LogicalTypeTestCase { + static final OffsetDateTime VALUE = OffsetDateTime.ofInstant( + Instant.ofEpochMilli(1526955327123L), + ZoneId.of("UTC") + ); + + @Override + protected Class dataClass() { + return TestCase.class; + } + + @Override + protected Schema.Type schemaType() { + return Schema.Type.LONG; + } + + @Override + protected String logicalType() { + return "timestamp-micros"; + } + + @Override + protected TestCase testData() { + TestCase v = new TestCase(); + v.value = VALUE; + return v; + } + + @Override + protected Object convertedValue() { + return 1526955327123L * 1000L; + } + + + static class TestCase extends TestData { + @JsonProperty(required = true) + @AvroType(schemaType = Schema.Type.LONG, logicalType = AvroType.LogicalType.TIMESTAMP_MICROSECOND) + OffsetDateTime value; + + @Override + public OffsetDateTime value() { + return this.value; + } + } + +} diff --git a/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMicrosZonedDateTimeTest.java b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMicrosZonedDateTimeTest.java new file mode 100644 index 000000000..0dccaf944 --- /dev/null +++ b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMicrosZonedDateTimeTest.java @@ -0,0 +1,64 @@ +package com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.time; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import com.fasterxml.jackson.dataformat.avro.AvroMapper; +import com.fasterxml.jackson.dataformat.avro.java8.AvroJavaTimeModule; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.LogicalTypeTestCase; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.TestData; +import org.apache.avro.Schema; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +public class TimestampMicrosZonedDateTimeTest extends LogicalTypeTestCase { + static final ZonedDateTime VALUE = ZonedDateTime.ofInstant( + Instant.ofEpochMilli(1526955327123L), + ZoneId.of("UTC") + ); + + @Override + protected Class dataClass() { + return TestCase.class; + } + + @Override + protected Schema.Type schemaType() { + return Schema.Type.LONG; + } + + @Override + protected String logicalType() { + return "timestamp-micros"; + } + + @Override + protected TestCase testData() { + TestCase v = new TestCase(); + v.value = VALUE; + return v; + } + + @Override + protected Object convertedValue() { + return 1526955327123L * 1000L; + } + + @Override + protected void configure(AvroMapper mapper) { + mapper.registerModule(new AvroJavaTimeModule()); + } + + static class TestCase extends TestData { + @JsonProperty(required = true) + @AvroType(schemaType = Schema.Type.LONG, logicalType = AvroType.LogicalType.TIMESTAMP_MICROSECOND) + ZonedDateTime value; + + @Override + public ZonedDateTime value() { + return this.value; + } + } + +} diff --git a/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMillisDateTest.java b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMillisDateTest.java new file mode 100644 index 000000000..efd80196f --- /dev/null +++ b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMillisDateTest.java @@ -0,0 +1,58 @@ +package com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.time; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import com.fasterxml.jackson.dataformat.avro.AvroMapper; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.LogicalTypeTestCase; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.TestData; +import org.apache.avro.Schema; + +import java.util.Date; + +public class TimestampMillisDateTest extends LogicalTypeTestCase { + static final Date VALUE = new Date(1526955327123L); + + @Override + protected Class dataClass() { + return TestCase.class; + } + + @Override + protected Schema.Type schemaType() { + return Schema.Type.LONG; + } + + @Override + protected String logicalType() { + return "timestamp-millis"; + } + + @Override + protected TestCase testData() { + TestCase v = new TestCase(); + v.value = VALUE; + return v; + } + + @Override + protected Object convertedValue() { + return 1526955327123L; + } + + @Override + protected void configure(AvroMapper mapper) { + + } + + static class TestCase extends TestData { + @JsonProperty(required = true) + @AvroType(schemaType = Schema.Type.LONG, logicalType = AvroType.LogicalType.TIMESTAMP_MILLISECOND) + Date value; + + @Override + public Date value() { + return this.value; + } + } + +} diff --git a/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMillisInstantTest.java b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMillisInstantTest.java new file mode 100644 index 000000000..3d0e9aca7 --- /dev/null +++ b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMillisInstantTest.java @@ -0,0 +1,59 @@ +package com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.time; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.avro.AvroMapper; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.LogicalTypeTestCase; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.TestData; +import org.apache.avro.Schema; + +import java.time.Instant; +import java.util.Date; + +public class TimestampMillisInstantTest extends LogicalTypeTestCase { + static final Instant VALUE = new Date(1526955327123L).toInstant(); + + @Override + protected Class dataClass() { + return TestCase.class; + } + + @Override + protected Schema.Type schemaType() { + return Schema.Type.LONG; + } + + @Override + protected String logicalType() { + return "timestamp-millis"; + } + + @Override + protected TestCase testData() { + TestCase v = new TestCase(); + v.value = VALUE; + return v; + } + + @Override + protected Object convertedValue() { + return 1526955327123L; + } + + @Override + protected void configure(AvroMapper mapper) { + + } + + static class TestCase extends TestData { + @JsonProperty(required = true) + @AvroType(schemaType = Schema.Type.LONG, logicalType = AvroType.LogicalType.TIMESTAMP_MILLISECOND) + Instant value; + + @Override + public Instant value() { + return this.value; + } + } + +} diff --git a/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMillisLocalDateTimeTest.java b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMillisLocalDateTimeTest.java new file mode 100644 index 000000000..0d44139e6 --- /dev/null +++ b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMillisLocalDateTimeTest.java @@ -0,0 +1,58 @@ +package com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.time; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.LogicalTypeTestCase; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.TestData; +import org.apache.avro.Schema; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; + +public class TimestampMillisLocalDateTimeTest extends LogicalTypeTestCase { + static final LocalDateTime VALUE = LocalDateTime.ofInstant( + Instant.ofEpochMilli(1526955327123L), + ZoneId.of("UTC") + ); + + @Override + protected Class dataClass() { + return TestCase.class; + } + + @Override + protected Schema.Type schemaType() { + return Schema.Type.LONG; + } + + @Override + protected String logicalType() { + return "timestamp-millis"; + } + + @Override + protected TestCase testData() { + TestCase v = new TestCase(); + v.value = VALUE; + return v; + } + + @Override + protected Object convertedValue() { + return 1526955327123L; + } + + static class TestCase extends TestData { + @JsonProperty(required = true) + @AvroType(schemaType = Schema.Type.LONG, logicalType = AvroType.LogicalType.TIMESTAMP_MILLISECOND) + + LocalDateTime value; + + @Override + public LocalDateTime value() { + return this.value; + } + } + +} diff --git a/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMillisOffsetDateTimeTest.java b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMillisOffsetDateTimeTest.java new file mode 100644 index 000000000..db2e9ee8c --- /dev/null +++ b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMillisOffsetDateTimeTest.java @@ -0,0 +1,63 @@ +package com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.time; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import com.fasterxml.jackson.dataformat.avro.AvroMapper; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.LogicalTypeTestCase; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.TestData; +import org.apache.avro.Schema; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; + +public class TimestampMillisOffsetDateTimeTest extends LogicalTypeTestCase { + static final OffsetDateTime VALUE = OffsetDateTime.ofInstant( + Instant.ofEpochMilli(1526955327123L), + ZoneId.of("UTC") + ); + + @Override + protected Class dataClass() { + return TestCase.class; + } + + @Override + protected Schema.Type schemaType() { + return Schema.Type.LONG; + } + + @Override + protected String logicalType() { + return "timestamp-millis"; + } + + @Override + protected TestCase testData() { + TestCase v = new TestCase(); + v.value = VALUE; + return v; + } + + @Override + protected Object convertedValue() { + return 1526955327123L; + } + + @Override + protected void configure(AvroMapper mapper) { + + } + + static class TestCase extends TestData { + @JsonProperty(required = true) + @AvroType(schemaType = Schema.Type.LONG, logicalType = AvroType.LogicalType.TIMESTAMP_MILLISECOND) + OffsetDateTime value; + + @Override + public OffsetDateTime value() { + return this.value; + } + } + +} diff --git a/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMillisZonedDateTimeTest.java b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMillisZonedDateTimeTest.java new file mode 100644 index 000000000..a3d4798e6 --- /dev/null +++ b/avro-java8/src/test/java/com/fasterxml/jackson/dataformat/avro/java8/logicaltypes/time/TimestampMillisZonedDateTimeTest.java @@ -0,0 +1,57 @@ +package com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.time; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.LogicalTypeTestCase; +import com.fasterxml.jackson.dataformat.avro.java8.logicaltypes.TestData; +import org.apache.avro.Schema; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +public class TimestampMillisZonedDateTimeTest extends LogicalTypeTestCase { + static final ZonedDateTime VALUE = ZonedDateTime.ofInstant( + Instant.ofEpochMilli(1526955327123L), + ZoneId.of("UTC") + ); + + @Override + protected Class dataClass() { + return TestCase.class; + } + + @Override + protected Schema.Type schemaType() { + return Schema.Type.LONG; + } + + @Override + protected String logicalType() { + return "timestamp-millis"; + } + + @Override + protected TestCase testData() { + TestCase v = new TestCase(); + v.value = VALUE; + return v; + } + + @Override + protected Object convertedValue() { + return 1526955327123L; + } + + static class TestCase extends TestData { + @JsonProperty(required = true) + @AvroType(schemaType = Schema.Type.LONG, logicalType = AvroType.LogicalType.TIMESTAMP_MILLISECOND) + ZonedDateTime value; + + @Override + public ZonedDateTime value() { + return this.value; + } + } + +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroAnnotationIntrospector.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroAnnotationIntrospector.java index 6415a25f3..b2f1055ee 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroAnnotationIntrospector.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroAnnotationIntrospector.java @@ -1,11 +1,5 @@ package com.fasterxml.jackson.dataformat.avro; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.apache.avro.reflect.*; - import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.core.Version; @@ -22,8 +16,35 @@ import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.databind.util.ClassUtil; import com.fasterxml.jackson.dataformat.avro.apacheimpl.CustomEncodingDeserializer; +import com.fasterxml.jackson.dataformat.avro.deser.AvroDateDateDeserializer; +import com.fasterxml.jackson.dataformat.avro.deser.AvroDateTimeDeserializer; +import com.fasterxml.jackson.dataformat.avro.deser.AvroDateTimestampDeserializer; +import com.fasterxml.jackson.dataformat.avro.deser.AvroDecimalDeserializer; +import com.fasterxml.jackson.dataformat.avro.deser.AvroUUIDDeserializer; import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaHelper; +import com.fasterxml.jackson.dataformat.avro.ser.AvroBytesDecimalSerializer; +import com.fasterxml.jackson.dataformat.avro.ser.AvroDateDateSerializer; +import com.fasterxml.jackson.dataformat.avro.ser.AvroDateTimeSerializer; +import com.fasterxml.jackson.dataformat.avro.ser.AvroDateTimestampSerializer; +import com.fasterxml.jackson.dataformat.avro.ser.AvroFixedDecimalSerializer; +import com.fasterxml.jackson.dataformat.avro.ser.AvroUUIDSerializer; import com.fasterxml.jackson.dataformat.avro.ser.CustomEncodingSerializer; +import org.apache.avro.reflect.AvroAlias; +import org.apache.avro.reflect.AvroDefault; +import org.apache.avro.reflect.AvroEncode; +import org.apache.avro.reflect.AvroIgnore; +import org.apache.avro.reflect.AvroName; +import org.apache.avro.reflect.CustomEncoding; +import org.apache.avro.reflect.Nullable; +import org.apache.avro.reflect.Stringable; +import org.apache.avro.reflect.Union; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.UUID; /** * Adds support for the following annotations from the Apache Avro implementation: @@ -31,8 +52,8 @@ *
  • {@link AvroIgnore @AvroIgnore} - Alias for JsonIgnore
  • *
  • {@link AvroName @AvroName("custom Name")} - Alias for JsonProperty("custom name")
  • *
  • {@link AvroDefault @AvroDefault("default value")} - Alias for JsonProperty.defaultValue, to - * define default value for generated Schemas - *
  • + * define default value for generated Schemas + * *
  • {@link Nullable @Nullable} - Alias for JsonProperty(required = false)
  • *
  • {@link Stringable @Stringable} - Alias for JsonCreator on the constructor and JsonValue on * the {@link #toString()} method.
  • @@ -41,156 +62,235 @@ * * @since 2.9 */ -public class AvroAnnotationIntrospector extends AnnotationIntrospector -{ - private static final long serialVersionUID = 1L; +public class AvroAnnotationIntrospector extends AnnotationIntrospector { + private static final long serialVersionUID = 1L; - public AvroAnnotationIntrospector() { } + public AvroAnnotationIntrospector() { + } - @Override - public Version version() { - return PackageVersion.VERSION; - } + @Override + public Version version() { + return PackageVersion.VERSION; + } - @Override - public boolean hasIgnoreMarker(AnnotatedMember m) { - return _findAnnotation(m, AvroIgnore.class) != null; - } + @Override + public boolean hasIgnoreMarker(AnnotatedMember m) { + return _findAnnotation(m, AvroIgnore.class) != null; + } - @Override - public PropertyName findNameForSerialization(Annotated a) { - return _findName(a); - } + @Override + public PropertyName findNameForSerialization(Annotated a) { + return _findName(a); + } - @Override - public PropertyName findNameForDeserialization(Annotated a) { - return _findName(a); - } + @Override + public PropertyName findNameForDeserialization(Annotated a) { + return _findName(a); + } - @Override - public Object findDeserializer(Annotated am) { - AvroEncode ann = _findAnnotation(am, AvroEncode.class); - if (ann != null) { - return new CustomEncodingDeserializer<>((CustomEncoding)ClassUtil.createInstance(ann.using(), true)); - } - return null; + @Override + public Object findDeserializer(Annotated a) { + AvroEncode ann = _findAnnotation(a, AvroEncode.class); + if (ann != null) { + return new CustomEncodingDeserializer<>((CustomEncoding) ClassUtil.createInstance(ann.using(), true)); } - @Override - public String findPropertyDefaultValue(Annotated m) { - AvroDefault ann = _findAnnotation(m, AvroDefault.class); - return (ann == null) ? null : ann.value(); - } + AvroType logicalType = _findAnnotation(a, AvroType.class); - @Override - public List findPropertyAliases(Annotated m) { - AvroAlias ann = _findAnnotation(m, AvroAlias.class); - if (ann == null) { - return null; - } - return Collections.singletonList(PropertyName.construct(ann.alias())); + if(null != logicalType) { + switch (logicalType.logicalType()) { + case DECIMAL: + if (a.getRawType().isAssignableFrom(BigDecimal.class)) { + return new AvroDecimalDeserializer(logicalType.scale()); + } + case TIME_MILLISECOND: + if (a.getRawType().isAssignableFrom(Date.class)) { + return AvroDateTimeDeserializer.MILLIS; + } + case TIMESTAMP_MILLISECOND: + if (a.getRawType().isAssignableFrom(Date.class)) { + return AvroDateTimestampDeserializer.MILLIS; + } + case TIME_MICROSECOND: + if (a.getRawType().isAssignableFrom(Date.class)) { + return AvroDateTimeDeserializer.MICROS; + } + case TIMESTAMP_MICROSECOND: + if (a.getRawType().isAssignableFrom(Date.class)) { + return AvroDateTimestampDeserializer.MICROS; + } + case UUID: + if (a.getRawType().isAssignableFrom(UUID.class)) { + return AvroUUIDDeserializer.INSTANCE; + } + case DATE: + if (a.getRawType().isAssignableFrom(Date.class)) { + return AvroDateDateDeserializer.INSTANCE; + } + } } + return null; + } - protected PropertyName _findName(Annotated a) - { - AvroName ann = _findAnnotation(a, AvroName.class); - return (ann == null) ? null : PropertyName.construct(ann.value()); - } + @Override + public String findPropertyDefaultValue(Annotated m) { + AvroDefault ann = _findAnnotation(m, AvroDefault.class); + return (ann == null) ? null : ann.value(); + } - @Override - public Boolean hasRequiredMarker(AnnotatedMember m) { - if (_hasAnnotation(m, Nullable.class)) { - return Boolean.FALSE; - } - return null; + @Override + public List findPropertyAliases(Annotated m) { + AvroAlias ann = _findAnnotation(m, AvroAlias.class); + if (ann == null) { + return null; } + return Collections.singletonList(PropertyName.construct(ann.alias())); + } - @Override - public JsonCreator.Mode findCreatorAnnotation(MapperConfig config, Annotated a) { - if (a instanceof AnnotatedConstructor) { - AnnotatedConstructor constructor = (AnnotatedConstructor) a; - // 09-Mar-2017, tatu: Ideally would allow mix-ins etc, but for now let's take - // a short-cut here: - Class declClass = constructor.getDeclaringClass(); - if (declClass.getAnnotation(Stringable.class) != null) { - if (constructor.getParameterCount() == 1 - && String.class.equals(constructor.getRawParameterType(0))) { - return JsonCreator.Mode.DELEGATING; - } - } - } - return null; - } + protected PropertyName _findName(Annotated a) { + AvroName ann = _findAnnotation(a, AvroName.class); + return (ann == null) ? null : PropertyName.construct(ann.value()); + } - @Override - public Object findSerializer(Annotated a) { - if (a.hasAnnotation(Stringable.class)) { - return ToStringSerializer.class; - } - AvroEncode ann = _findAnnotation(a, AvroEncode.class); - if (ann != null) { - return new CustomEncodingSerializer<>((CustomEncoding)ClassUtil.createInstance(ann.using(), true)); - } - return null; + @Override + public Boolean hasRequiredMarker(AnnotatedMember m) { + if (_hasAnnotation(m, Nullable.class)) { + return Boolean.FALSE; } + return null; + } - @Override - public List findSubtypes(Annotated a) - { - Class[] types = _getUnionTypes(a); - if (types == null) { - return null; + @Override + public JsonCreator.Mode findCreatorAnnotation(MapperConfig config, Annotated a) { + if (a instanceof AnnotatedConstructor) { + AnnotatedConstructor constructor = (AnnotatedConstructor) a; + // 09-Mar-2017, tatu: Ideally would allow mix-ins etc, but for now let's take + // a short-cut here: + Class declClass = constructor.getDeclaringClass(); + if (declClass.getAnnotation(Stringable.class) != null) { + if (constructor.getParameterCount() == 1 + && String.class.equals(constructor.getRawParameterType(0))) { + return JsonCreator.Mode.DELEGATING; } - ArrayList names = new ArrayList<>(types.length); - for (Class subtype : types) { - names.add(new NamedType(subtype, AvroSchemaHelper.getTypeId(subtype))); - } - return names; + } } + return null; + } - @Override - public TypeResolverBuilder findTypeResolver(MapperConfig config, AnnotatedClass ac, JavaType baseType) { - return _findTypeResolver(config, ac, baseType); + @Override + public Object findSerializer(Annotated a) { + if (a.hasAnnotation(Stringable.class)) { + return ToStringSerializer.class; } - - @Override - public TypeResolverBuilder findPropertyTypeResolver(MapperConfig config, AnnotatedMember am, JavaType baseType) { - return _findTypeResolver(config, am, baseType); + AvroEncode ann = _findAnnotation(a, AvroEncode.class); + if (ann != null) { + return new CustomEncodingSerializer<>((CustomEncoding) ClassUtil.createInstance(ann.using(), true)); } + AvroType logicalType = _findAnnotation(a, AvroType.class); + + if(null != logicalType) { + switch (logicalType.logicalType()) { + case DECIMAL: + switch (logicalType.schemaType()) { + case FIXED: + if (a.getRawType().isAssignableFrom(BigDecimal.class)) { + return new AvroFixedDecimalSerializer(logicalType.scale(), logicalType.fixedSize()); + } + case BYTES: + if (a.getRawType().isAssignableFrom(BigDecimal.class)) { + return new AvroBytesDecimalSerializer(logicalType.scale()); + } + default: + throw new UnsupportedOperationException( + String.format("%s is not a supported type for the logical type 'decimal'", logicalType.schemaType()) + ); + } + case TIME_MILLISECOND: + if (a.getRawType().isAssignableFrom(Date.class)) { + return AvroDateTimeSerializer.MILLIS; + } + case TIMESTAMP_MILLISECOND: + if (a.getRawType().isAssignableFrom(Date.class)) { + return AvroDateTimestampSerializer.MILLIS; + } + case TIME_MICROSECOND: + if (a.getRawType().isAssignableFrom(Date.class)) { + return AvroDateTimeSerializer.MICROS; + } + case TIMESTAMP_MICROSECOND: + if (a.getRawType().isAssignableFrom(Date.class)) { + return AvroDateTimestampSerializer.MICROS; + } + case UUID: + if (a.getRawType().isAssignableFrom(UUID.class)) { + return AvroUUIDSerializer.INSTANCE; + } + case DATE: + if (a.getRawType().isAssignableFrom(Date.class)) { + return AvroDateDateSerializer.INSTANCE; + } + } + } + + return null; + } - @Override - public TypeResolverBuilder findPropertyContentTypeResolver(MapperConfig config, AnnotatedMember am, JavaType containerType) { - return _findTypeResolver(config, am, containerType); + @Override + public List findSubtypes(Annotated a) { + Class[] types = _getUnionTypes(a); + if (types == null) { + return null; + } + ArrayList names = new ArrayList<>(types.length); + for (Class subtype : types) { + names.add(new NamedType(subtype, AvroSchemaHelper.getTypeId(subtype))); } + return names; + } - protected TypeResolverBuilder _findTypeResolver(MapperConfig config, Annotated ann, JavaType baseType) { - // 14-Apr-2017, tatu: There are two ways to enable polymorphic typing, above and beyond - // basic Jackson: use of `@Union`, and "default typing" approach for `java.lang.Object`: - // latter since Avro support for "untyped" values is otherwise difficult. - // This seems to work for now, but maybe needs more work in future... - if (baseType.isJavaLangObject() || (_getUnionTypes(ann) != null)) { - TypeResolverBuilder resolver = new AvroTypeResolverBuilder(); - JsonTypeInfo typeInfo = ann.getAnnotation(JsonTypeInfo.class); - if (typeInfo != null && typeInfo.defaultImpl() != JsonTypeInfo.class) { - resolver = resolver.defaultImpl(typeInfo.defaultImpl()); - } - return resolver; - } - return null; + @Override + public TypeResolverBuilder findTypeResolver(MapperConfig config, AnnotatedClass ac, JavaType baseType) { + return _findTypeResolver(config, ac, baseType); + } + + @Override + public TypeResolverBuilder findPropertyTypeResolver(MapperConfig config, AnnotatedMember am, JavaType baseType) { + return _findTypeResolver(config, am, baseType); + } + + @Override + public TypeResolverBuilder findPropertyContentTypeResolver(MapperConfig config, AnnotatedMember am, JavaType containerType) { + return _findTypeResolver(config, am, containerType); + } + + protected TypeResolverBuilder _findTypeResolver(MapperConfig config, Annotated ann, JavaType baseType) { + // 14-Apr-2017, tatu: There are two ways to enable polymorphic typing, above and beyond + // basic Jackson: use of `@Union`, and "default typing" approach for `java.lang.Object`: + // latter since Avro support for "untyped" values is otherwise difficult. + // This seems to work for now, but maybe needs more work in future... + if (baseType.isJavaLangObject() || (_getUnionTypes(ann) != null)) { + TypeResolverBuilder resolver = new AvroTypeResolverBuilder(); + JsonTypeInfo typeInfo = ann.getAnnotation(JsonTypeInfo.class); + if (typeInfo != null && typeInfo.defaultImpl() != JsonTypeInfo.class) { + resolver = resolver.defaultImpl(typeInfo.defaultImpl()); + } + return resolver; } + return null; + } - protected Class[] _getUnionTypes(Annotated a) { - Union ann = _findAnnotation(a, Union.class); - if (ann != null) { - // 14-Apr-2017, tatu: I think it makes sense to require non-empty List, as this allows - // disabling annotation with overrides. But one could even consider requiring more than - // one (where single type is not really polymorphism)... for now, however, just one - // is acceptable, and maybe that has valid usages. - Class[] c = ann.value(); - if (c.length > 0) { - return c; - } - } - return null; + protected Class[] _getUnionTypes(Annotated a) { + Union ann = _findAnnotation(a, Union.class); + if (ann != null) { + // 14-Apr-2017, tatu: I think it makes sense to require non-empty List, as this allows + // disabling annotation with overrides. But one could even consider requiring more than + // one (where single type is not really polymorphism)... for now, however, just one + // is acceptable, and maybe that has valid usages. + Class[] c = ann.value(); + if (c.length > 0) { + return c; + } } + return null; + } } diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroFixedSize.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroFixedSize.java index 81210aa31..d5ed22b7f 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroFixedSize.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroFixedSize.java @@ -10,6 +10,7 @@ */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.FIELD}) +@Deprecated public @interface AvroFixedSize { /** * The name of the type in the generated schema diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroType.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroType.java new file mode 100644 index 000000000..53566ee76 --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroType.java @@ -0,0 +1,58 @@ +package com.fasterxml.jackson.dataformat.avro; + +import org.apache.avro.Schema; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.FIELD}) +public @interface AvroType { + /** + * + */ + Schema.Type schemaType(); + + /** + * + */ + LogicalType logicalType() default LogicalType.NONE; + + /** + * The name of the type in the generated schema + */ + String typeName() default ""; + + /** + * The namespace of the type in the generated schema (optional) + */ + String typeNamespace() default ""; + + /** + * The size when the schemaType is FIXED. + */ + int fixedSize() default 0; + /** + * The maximum precision of decimals stored in this type. + */ + int precision() default 0; + + /** + * + * @return + */ + int scale() default 0; + + enum LogicalType { + DECIMAL, + DATE, + TIME_MICROSECOND, + TIMESTAMP_MICROSECOND, + TIME_MILLISECOND, + TIMESTAMP_MILLISECOND, + UUID, + NONE + } +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/apacheimpl/ApacheAvroParserImpl.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/apacheimpl/ApacheAvroParserImpl.java index 97af3aed4..4214d96d0 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/apacheimpl/ApacheAvroParserImpl.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/apacheimpl/ApacheAvroParserImpl.java @@ -3,6 +3,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.Writer; +import java.math.BigDecimal; +import java.math.BigInteger; import org.apache.avro.io.BinaryDecoder; @@ -372,6 +374,32 @@ public int decodeEnum() throws IOException { return (_enumIndex = _decoder.readEnum()); } + @Override + public JsonToken decodeBytesDecimal(int scale) throws IOException { + decodeBytes(); + _numberBigDecimal = new BigDecimal(new BigInteger(_binaryValue), scale); + _numTypesValid = NR_BIGDECIMAL; + return JsonToken.VALUE_NUMBER_FLOAT; + } + + @Override + public void skipBytesDecimal() throws IOException { + skipBytes(); + } + + @Override + public JsonToken decodeFixedDecimal(int scale, int size) throws IOException { + decodeFixed(size); + _numberBigDecimal = new BigDecimal(new BigInteger(_binaryValue), scale); + _numTypesValid = NR_BIGDECIMAL; + return JsonToken.VALUE_NUMBER_FLOAT; + } + + @Override + public void skipFixedDecimal(int size) throws IOException { + skipFixed(size); + } + /* /********************************************************** /* Methods for AvroReadContext impls, other diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroDateDateDeserializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroDateDateDeserializer.java new file mode 100644 index 000000000..3b7df965c --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroDateDateDeserializer.java @@ -0,0 +1,25 @@ +package com.fasterxml.jackson.dataformat.avro.deser; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.util.Date; + + +public class AvroDateDateDeserializer extends JsonDeserializer { + public static final JsonDeserializer INSTANCE = new AvroDateDateDeserializer(); + private static final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000; + + AvroDateDateDeserializer() { + + } + + @Override + public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + final long input = jsonParser.getLongValue(); + return new java.util.Date(input * MILLIS_PER_DAY); + } +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroDateTimeDeserializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroDateTimeDeserializer.java new file mode 100644 index 000000000..1efbc575e --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroDateTimeDeserializer.java @@ -0,0 +1,36 @@ +package com.fasterxml.jackson.dataformat.avro.deser; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.util.Date; +import java.util.concurrent.TimeUnit; + + +public class AvroDateTimeDeserializer extends JsonDeserializer { + public static final JsonDeserializer MILLIS = new AvroDateTimeDeserializer(TimeUnit.MILLISECONDS); + public static final JsonDeserializer MICROS = new AvroDateTimeDeserializer(TimeUnit.MICROSECONDS); + private final TimeUnit resolution; + private final long max; + + AvroDateTimeDeserializer(TimeUnit resolution) { + this.resolution = resolution; + this.max = this.resolution.convert(86400, TimeUnit.SECONDS); + } + + @Override + public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + final long input = jsonParser.getLongValue(); + + if (input < 0 || input > this.max) { + throw new IllegalStateException( + String.format("Value must be between 0 and %s %s(s).", this.max, this.resolution) + ); + } + final long output = TimeUnit.MILLISECONDS.convert(input, this.resolution); + return new Date(output); + } +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroDateTimestampDeserializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroDateTimestampDeserializer.java new file mode 100644 index 000000000..c0086420c --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroDateTimestampDeserializer.java @@ -0,0 +1,40 @@ +package com.fasterxml.jackson.dataformat.avro.deser; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.util.Date; +import java.util.concurrent.TimeUnit; + + +public class AvroDateTimestampDeserializer extends JsonDeserializer { + public static final JsonDeserializer MILLIS = new AvroDateTimestampDeserializer(TimeUnit.MILLISECONDS); + public static final JsonDeserializer MICROS = new AvroDateTimestampDeserializer(TimeUnit.MICROSECONDS); + private final TimeUnit resolution; + + AvroDateTimestampDeserializer(TimeUnit resolution) { + this.resolution = resolution; + } + + @Override + public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + final long input = jsonParser.getLongValue(); + final long output; + switch (this.resolution) { + case MICROSECONDS: + output = TimeUnit.MICROSECONDS.toMillis(input); + break; + case MILLISECONDS: + output = input; + break; + default: + throw new UnsupportedOperationException( + String.format("%s is not supported", this.resolution) + ); + } + return new Date(output); + } +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroDecimalDeserializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroDecimalDeserializer.java new file mode 100644 index 000000000..91d0259c1 --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroDecimalDeserializer.java @@ -0,0 +1,24 @@ +package com.fasterxml.jackson.dataformat.avro.deser; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; + +public class AvroDecimalDeserializer extends JsonDeserializer { + private final int scale; + + public AvroDecimalDeserializer(int scale) { + this.scale = scale; + } + + @Override + public BigDecimal deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + final byte[] bytes = jsonParser.getBinaryValue(); + return new BigDecimal(new BigInteger(bytes), this.scale); + } +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroParserImpl.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroParserImpl.java index 5e045dea8..a5573c1a7 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroParserImpl.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroParserImpl.java @@ -560,6 +560,17 @@ public long getRemainingElements() public abstract int decodeIndex() throws IOException; public abstract int decodeEnum() throws IOException; + /* + /********************************************************** + /* Methods for AvroReadContext implementations: decimals + /********************************************************** + */ + + public abstract JsonToken decodeBytesDecimal(int scale) throws IOException; + public abstract void skipBytesDecimal() throws IOException; + public abstract JsonToken decodeFixedDecimal(int scale, int size) throws IOException; + public abstract void skipFixedDecimal(int size) throws IOException; + /* /********************************************************** /* Methods for AvroReadContext impls, other diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroReaderFactory.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroReaderFactory.java index 517e7fc2c..8a9d7d30c 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroReaderFactory.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroReaderFactory.java @@ -2,26 +2,25 @@ import java.util.*; +import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaHelper; +import org.apache.avro.LogicalTypes; import org.apache.avro.Schema; import org.apache.avro.util.internal.JacksonUtils; -import com.fasterxml.jackson.dataformat.avro.deser.ScalarDecoder.*; -import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaHelper; - /** * Helper class used for constructing a hierarchic reader for given * (reader-) schema. */ public abstract class AvroReaderFactory { - protected final static ScalarDecoder READER_BOOLEAN = new BooleanDecoder(); - protected final static ScalarDecoder READER_BYTES = new BytesDecoder(); - protected final static ScalarDecoder READER_DOUBLE = new DoubleReader(); - protected final static ScalarDecoder READER_FLOAT = new FloatReader(); - protected final static ScalarDecoder READER_INT = new IntReader(); - protected final static ScalarDecoder READER_LONG = new LongReader(); - protected final static ScalarDecoder READER_NULL = new NullReader(); - protected final static ScalarDecoder READER_STRING = new StringReader(); + protected final static ScalarDecoder READER_BOOLEAN = new ScalarDecoder.BooleanDecoder(); + protected final static ScalarDecoder READER_BYTES = new ScalarDecoder.BytesDecoder(); + protected final static ScalarDecoder READER_DOUBLE = new ScalarDecoder.DoubleReader(); + protected final static ScalarDecoder READER_FLOAT = new ScalarDecoder.FloatReader(); + protected final static ScalarDecoder READER_INT = new ScalarDecoder.IntReader(); + protected final static ScalarDecoder READER_LONG = new ScalarDecoder.LongReader(); + protected final static ScalarDecoder READER_NULL = new ScalarDecoder.NullReader(); + protected final static ScalarDecoder READER_STRING = new ScalarDecoder.StringReader(); /** * To resolve cyclic types, need to keep track of resolved named @@ -56,19 +55,32 @@ public ScalarDecoder createScalarValueDecoder(Schema type) switch (type.getType()) { case BOOLEAN: return READER_BOOLEAN; - case BYTES: + case BYTES: + if(type.getLogicalType() != null && "decimal".equals(type.getLogicalType().getName())) { + LogicalTypes.Decimal decimal = (LogicalTypes.Decimal) type.getLogicalType(); + return new ScalarDecoder.BytesDecimalReader( + decimal.getScale() + ); + } return READER_BYTES; case DOUBLE: return READER_DOUBLE; case ENUM: - return new EnumDecoder(AvroSchemaHelper.getFullName(type), type.getEnumSymbols()); - case FIXED: - return new FixedDecoder(type.getFixedSize(), AvroSchemaHelper.getFullName(type)); + return new ScalarDecoder.EnumDecoder(AvroSchemaHelper.getFullName(type), type.getEnumSymbols()); + case FIXED: + if(type.getLogicalType() != null && "decimal".equals(type.getLogicalType().getName())) { + LogicalTypes.Decimal decimal = (LogicalTypes.Decimal) type.getLogicalType(); + return new ScalarDecoder.FixedDecimalReader( + decimal.getScale(), + type.getFixedSize() + ); + } + return new ScalarDecoder.FixedDecoder(type.getFixedSize(), AvroSchemaHelper.getFullName(type)); case FLOAT: return READER_FLOAT; case INT: if (AvroSchemaHelper.getTypeId(type) != null) { - return new IntReader(AvroSchemaHelper.getTypeId(type)); + return new ScalarDecoder.IntReader(AvroSchemaHelper.getTypeId(type)); } return READER_INT; case LONG: @@ -77,7 +89,7 @@ public ScalarDecoder createScalarValueDecoder(Schema type) return READER_NULL; case STRING: if (AvroSchemaHelper.getTypeId(type) != null) { - return new StringReader(AvroSchemaHelper.getTypeId(type)); + return new ScalarDecoder.StringReader(AvroSchemaHelper.getTypeId(type)); } return READER_STRING; case UNION: @@ -96,7 +108,7 @@ public ScalarDecoder createScalarValueDecoder(Schema type) } readers[i++] = reader; } - return new ScalarUnionDecoder(readers); + return new ScalarDecoder.ScalarUnionDecoder(readers); } case ARRAY: // ok to call just can't handle case MAP: diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroUUIDDeserializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroUUIDDeserializer.java new file mode 100644 index 000000000..fd00c14c8 --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroUUIDDeserializer.java @@ -0,0 +1,18 @@ +package com.fasterxml.jackson.dataformat.avro.deser; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.util.UUID; + +public class AvroUUIDDeserializer extends JsonDeserializer { + public static JsonDeserializer INSTANCE = new AvroUUIDDeserializer(); + @Override + public UUID deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + String value = jsonParser.getText(); + return UUID.fromString(value); + } +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/JacksonAvroParserImpl.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/JacksonAvroParserImpl.java index c4954845b..7dcca673c 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/JacksonAvroParserImpl.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/JacksonAvroParserImpl.java @@ -4,6 +4,8 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.Writer; +import java.math.BigDecimal; +import java.math.BigInteger; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.core.io.IOContext; @@ -993,6 +995,32 @@ public int decodeEnum() throws IOException { return (_enumIndex = decodeInt()); } + @Override + public JsonToken decodeBytesDecimal(int scale) throws IOException { + decodeBytes(); + _numberBigDecimal = new BigDecimal(new BigInteger(_binaryValue), scale); + _numTypesValid = NR_BIGDECIMAL; + return JsonToken.VALUE_NUMBER_FLOAT; + } + + @Override + public void skipBytesDecimal() throws IOException { + skipBytes(); + } + + @Override + public JsonToken decodeFixedDecimal(int scale, int size) throws IOException { + decodeFixed(size); + _numberBigDecimal = new BigDecimal(new BigInteger(_binaryValue), scale); + _numTypesValid = NR_BIGDECIMAL; + return JsonToken.VALUE_NUMBER_FLOAT; + } + + @Override + public void skipFixedDecimal(int size) throws IOException { + skipFixed(size); + } + @Override public boolean checkInputEnd() throws IOException { if (_closed) { diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ScalarDecoder.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ScalarDecoder.java index 55e67bd93..d8516fd15 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ScalarDecoder.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ScalarDecoder.java @@ -1,6 +1,7 @@ package com.fasterxml.jackson.dataformat.avro.deser; import java.io.IOException; +import java.math.BigDecimal; import java.util.List; import com.fasterxml.jackson.core.JsonToken; @@ -546,4 +547,100 @@ public void skipValue(AvroParserImpl parser) throws IOException { } } } + + protected final static class FixedDecimalReader extends ScalarDecoder { + private final int _scale; + private final int _size; + + public FixedDecimalReader(int scale, int size) { + _scale = scale; + _size = size; + } + + @Override + public JsonToken decodeValue(AvroParserImpl parser) throws IOException { + return parser.decodeFixedDecimal(_scale, _size); + } + + @Override + protected void skipValue(AvroParserImpl parser) throws IOException { + parser.skipFixedDecimal(_size); + } + + @Override + public String getTypeId() { + return AvroSchemaHelper.getTypeId(BigDecimal.class); + } + + @Override + public AvroFieldReader asFieldReader(String name, boolean skipper) { + return new FR(name, skipper, getTypeId(), _scale, _size); + } + + private final static class FR extends AvroFieldReader { + private final int _scale; + private final int _size; + public FR(String name, boolean skipper, String typeId, int scale, int size) { + super(name, skipper, typeId); + _scale = scale; + _size = size; + } + + @Override + public JsonToken readValue(AvroReadContext parent, AvroParserImpl parser) throws IOException { + return parser.decodeFixedDecimal(_scale, _size); + } + + @Override + public void skipValue(AvroParserImpl parser) throws IOException { + parser.skipFixedDecimal(_size); + } + } + } + + protected final static class BytesDecimalReader extends ScalarDecoder { + private final int _scale; + + public BytesDecimalReader(int scale) { + _scale = scale; + } + + @Override + public JsonToken decodeValue(AvroParserImpl parser) throws IOException { + return parser.decodeBytesDecimal(_scale); + } + + @Override + protected void skipValue(AvroParserImpl parser) throws IOException { + parser.skipBytesDecimal(); + } + + @Override + public String getTypeId() { + return AvroSchemaHelper.getTypeId(BigDecimal.class); + } + + @Override + public AvroFieldReader asFieldReader(String name, boolean skipper) { + return new FR(name, skipper, getTypeId(), _scale); + } + + private final static class FR extends AvroFieldReader { + private final int _scale; + public FR(String name, boolean skipper, String typeId, int scale) { + super(name, skipper, typeId); + _scale = scale; + } + + @Override + public JsonToken readValue(AvroReadContext parent, AvroParserImpl parser) throws IOException { + return parser.decodeBytesDecimal(_scale); + } + + @Override + public void skipValue(AvroParserImpl parser) throws IOException { + parser.skipFloat(); + } + } + } } diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java index f1d6867bb..3c2bb9ba5 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java @@ -5,6 +5,8 @@ import java.util.List; import java.util.Map; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import org.apache.avro.LogicalTypes; import org.apache.avro.Schema; import org.apache.avro.Schema.Type; import org.apache.avro.reflect.AvroMeta; @@ -36,9 +38,9 @@ public class RecordVisitor protected final boolean _overridden; protected Schema _avroSchema; - + protected List _fields = new ArrayList(); - + public RecordVisitor(SerializerProvider p, JavaType type, DefinedSchemas schemas) { super(p); @@ -75,7 +77,7 @@ public RecordVisitor(SerializerProvider p, JavaType type, DefinedSchemas schemas } schemas.addSchema(type, _avroSchema); } - + @Override public Schema builtAvroSchema() { if (!_overridden) { @@ -90,7 +92,7 @@ public Schema builtAvroSchema() { /* JsonObjectFormatVisitor implementation /********************************************************** */ - + @Override public void property(BeanProperty writer) throws JsonMappingException { @@ -142,7 +144,7 @@ public void optionalProperty(String name, JsonFormatVisitable handler, /* Internal methods /********************************************************************** */ - + protected Schema.Field schemaFieldForWriter(BeanProperty prop, boolean optional) throws JsonMappingException { Schema writerSchema; @@ -152,31 +154,81 @@ protected Schema.Field schemaFieldForWriter(BeanProperty prop, boolean optional) Schema.Parser parser = new Schema.Parser(); writerSchema = parser.parse(schemaOverride.value()); } else { - AvroFixedSize fixedSize = prop.getAnnotation(AvroFixedSize.class); - if (fixedSize != null) { - writerSchema = Schema.createFixed(fixedSize.typeName(), null, fixedSize.typeNamespace(), fixedSize.size()); - } else { - JsonSerializer ser = null; + AvroType type = prop.getAnnotation(AvroType.class); - // 23-Nov-2012, tatu: Ideally shouldn't need to do this but... - if (prop instanceof BeanPropertyWriter) { - BeanPropertyWriter bpw = (BeanPropertyWriter) prop; - ser = bpw.getSerializer(); - /* - * 2-Mar-2017, bryan: AvroEncode annotation expects to have the schema used directly - */ - optional = optional && !(ser instanceof CustomEncodingSerializer); // Don't modify schema + if(null != type) { + if(type.schemaType() == Type.FIXED) { + writerSchema = Schema.createFixed(type.typeName(), + null, + type.typeNamespace(), + type.fixedSize() + ); + } else { + writerSchema = Schema.create(type.schemaType()); + } + switch (type.logicalType()) { + case NONE: + break; + case DATE: + writerSchema = LogicalTypes.date() + .addToSchema(writerSchema); + break; + case UUID: + writerSchema = LogicalTypes.uuid() + .addToSchema(writerSchema); + break; + case DECIMAL: + writerSchema = LogicalTypes.decimal(type.precision(), type.scale()) + .addToSchema(writerSchema); + break; + case TIME_MICROSECOND: + writerSchema = LogicalTypes.timeMicros() + .addToSchema(writerSchema); + break; + case TIME_MILLISECOND: + writerSchema = LogicalTypes.timeMillis() + .addToSchema(writerSchema); + break; + case TIMESTAMP_MICROSECOND: + writerSchema = LogicalTypes.timestampMicros() + .addToSchema(writerSchema); + break; + case TIMESTAMP_MILLISECOND: + writerSchema = LogicalTypes.timestampMillis() + .addToSchema(writerSchema); + break; + default: + throw new UnsupportedOperationException( + String.format("%s is not a supported logical type.", type.logicalType()) + ); } - final SerializerProvider prov = getProvider(); - if (ser == null) { - if (prov == null) { - throw JsonMappingException.from(prov, "SerializerProvider missing for RecordVisitor"); + } else { + AvroFixedSize fixedSize = prop.getAnnotation(AvroFixedSize.class); + if (fixedSize != null) { + writerSchema = Schema.createFixed(fixedSize.typeName(), null, fixedSize.typeNamespace(), fixedSize.size()); + } else { + JsonSerializer ser = null; + + // 23-Nov-2012, tatu: Ideally shouldn't need to do this but... + if (prop instanceof BeanPropertyWriter) { + BeanPropertyWriter bpw = (BeanPropertyWriter) prop; + ser = bpw.getSerializer(); + /* + * 2-Mar-2017, bryan: AvroEncode annotation expects to have the schema used directly + */ + optional = optional && !(ser instanceof CustomEncodingSerializer); // Don't modify schema + } + final SerializerProvider prov = getProvider(); + if (ser == null) { + if (prov == null) { + throw JsonMappingException.from(prov, "SerializerProvider missing for RecordVisitor"); + } + ser = prov.findValueSerializer(prop.getType(), prop); } - ser = prov.findValueSerializer(prop.getType(), prop); + VisitorFormatWrapperImpl visitor = new VisitorFormatWrapperImpl(_schemas, prov); + ser.acceptJsonFormatVisitor(visitor, prop.getType()); + writerSchema = visitor.getAvroSchema(); } - VisitorFormatWrapperImpl visitor = new VisitorFormatWrapperImpl(_schemas, prov); - ser.acceptJsonFormatVisitor(visitor, prop.getType()); - writerSchema = visitor.getAvroSchema(); } /* 23-Nov-2012, tatu: Actually let's also assume that primitive type values diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroBytesDecimalSerializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroBytesDecimalSerializer.java new file mode 100644 index 000000000..37d6e9422 --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroBytesDecimalSerializer.java @@ -0,0 +1,27 @@ +package com.fasterxml.jackson.dataformat.avro.ser; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import org.codehaus.jackson.JsonGenerationException; + +import java.io.IOException; +import java.math.BigDecimal; + +public class AvroBytesDecimalSerializer extends JsonSerializer { + final int scale; + + public AvroBytesDecimalSerializer(int scale) { + this.scale = scale; + } + + @Override + public void serialize(BigDecimal decimal, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + if (scale != decimal.scale()) { + throw new JsonGenerationException( + String.format("Cannot encode decimal with scale %s as scale %s.", decimal.scale(), scale) + ); + } + jsonGenerator.writeBinary(decimal.unscaledValue().toByteArray()); + } +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroDateDateSerializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroDateDateSerializer.java new file mode 100644 index 000000000..ea8ef1adc --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroDateDateSerializer.java @@ -0,0 +1,29 @@ +package com.fasterxml.jackson.dataformat.avro.ser; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +public class AvroDateDateSerializer extends JsonSerializer { + public static final JsonSerializer INSTANCE = new AvroDateDateSerializer(); + private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + private static final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000; + + @Override + public void serialize(Date value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + Calendar calendar = Calendar.getInstance(UTC); + calendar.setTime(value); + if (calendar.get(Calendar.HOUR_OF_DAY) != 0 || calendar.get(Calendar.MINUTE) != 0 || + calendar.get(Calendar.SECOND) != 0 || calendar.get(Calendar.MILLISECOND) != 0) { + throw new IllegalStateException("Date type should not have any time fields set to non-zero values."); + } + long unixMillis = calendar.getTimeInMillis(); + int output =(int) (unixMillis / MILLIS_PER_DAY); + jsonGenerator.writeNumber(output); + } +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroDateTimeSerializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroDateTimeSerializer.java new file mode 100644 index 000000000..bedf4afde --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroDateTimeSerializer.java @@ -0,0 +1,40 @@ +package com.fasterxml.jackson.dataformat.avro.ser; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +public class AvroDateTimeSerializer extends JsonSerializer { + public final static JsonSerializer MILLIS = new AvroDateTimeSerializer(TimeUnit.MILLISECONDS); + public final static JsonSerializer MICROS = new AvroDateTimeSerializer(TimeUnit.MICROSECONDS); + private final static Date MIN_DATE = new Date(0L); + private final static Date MAX_DATE = new Date( + TimeUnit.SECONDS.toMillis(86400) + ); + + private final TimeUnit resolution; + private final long max; + + + AvroDateTimeSerializer(TimeUnit resolution) { + this.resolution = resolution; + this.max = this.resolution.convert(86400, TimeUnit.SECONDS); + } + + @Override + public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + final long input = date.getTime(); + + if (input < 0 || input > this.max) { + throw new IllegalStateException( + String.format("Value must be between %s and %s.", MIN_DATE, MAX_DATE) + ); + } + final long output = this.resolution.convert(input, TimeUnit.MILLISECONDS); + jsonGenerator.writeNumber(output); + } +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroDateTimestampSerializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroDateTimestampSerializer.java new file mode 100644 index 000000000..e40a42633 --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroDateTimestampSerializer.java @@ -0,0 +1,39 @@ +package com.fasterxml.jackson.dataformat.avro.ser; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +public class AvroDateTimestampSerializer extends JsonSerializer { + public final static JsonSerializer MILLIS = new AvroDateTimestampSerializer(TimeUnit.MILLISECONDS); + public final static JsonSerializer MICROS = new AvroDateTimestampSerializer(TimeUnit.MICROSECONDS); + + private final TimeUnit resolution; + + AvroDateTimestampSerializer(TimeUnit resolution) { + this.resolution = resolution; + } + + @Override + public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + final long input = date.getTime(); + final long output; + switch (this.resolution) { + case MICROSECONDS: + output = TimeUnit.MILLISECONDS.toMicros(input); + break; + case MILLISECONDS: + output = input; + break; + default: + throw new UnsupportedOperationException( + String.format("%s is not supported", this.resolution) + ); + } + jsonGenerator.writeNumber(output); + } +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroFixedDecimalSerializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroFixedDecimalSerializer.java new file mode 100644 index 000000000..e73ac83a5 --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroFixedDecimalSerializer.java @@ -0,0 +1,42 @@ +package com.fasterxml.jackson.dataformat.avro.ser; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import org.codehaus.jackson.JsonGenerationException; + +import java.io.IOException; +import java.math.BigDecimal; + +public class AvroFixedDecimalSerializer extends JsonSerializer { + final int scale; + final int fixedSize; + + public AvroFixedDecimalSerializer(int scale, int fixedSize) { + this.scale = scale; + this.fixedSize = fixedSize; + } + + @Override + public void serialize(BigDecimal value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + if (scale != value.scale()) { + throw new JsonGenerationException( + String.format("Cannot encode decimal with scale %s as scale %s.", value.scale(), scale) + ); + } + byte fillByte = (byte)(value.signum() < 0 ? 255 : 0); + byte[] unscaled = value.unscaledValue().toByteArray(); + byte[] bytes = new byte[this.fixedSize]; + int offset = bytes.length - unscaled.length; + + for(int i = 0; i < bytes.length; ++i) { + if (i < offset) { + bytes[i] = fillByte; + } else { + bytes[i] = unscaled[i - offset]; + } + } + + jsonGenerator.writeBinary(bytes); + } +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroUUIDSerializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroUUIDSerializer.java new file mode 100644 index 000000000..c2237044e --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroUUIDSerializer.java @@ -0,0 +1,17 @@ +package com.fasterxml.jackson.dataformat.avro.ser; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.util.UUID; + +public class AvroUUIDSerializer extends JsonSerializer { + public static final JsonSerializer INSTANCE = new AvroUUIDSerializer(); + + @Override + public void serialize(UUID uuid, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeString(uuid.toString()); + } +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/NonBSGenericDatumWriter.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/NonBSGenericDatumWriter.java index f7b87a99c..1bd39fa73 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/NonBSGenericDatumWriter.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/NonBSGenericDatumWriter.java @@ -1,20 +1,21 @@ package com.fasterxml.jackson.dataformat.avro.ser; -import java.io.IOException; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.ArrayList; - +import org.apache.avro.Conversions; import org.apache.avro.Schema; import org.apache.avro.Schema.Type; import org.apache.avro.generic.GenericData; import org.apache.avro.generic.GenericDatumWriter; import org.apache.avro.io.Encoder; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; + /** * Need to sub-class to prevent encoder from crapping on writing an optional * Enum value (see [dataformat-avro#12]) - * + * * @since 2.5 */ public class NonBSGenericDatumWriter @@ -25,7 +26,9 @@ public class NonBSGenericDatumWriter private final static Class CLS_STRING = String.class; private final static Class CLS_BIG_DECIMAL = BigDecimal.class; private final static Class CLS_BIG_INTEGER = BigInteger.class; - + private final static Conversions.DecimalConversion DECIMAL_CONVERSION = new Conversions.DecimalConversion(); + + public NonBSGenericDatumWriter(Schema root) { super(root); } @@ -57,6 +60,10 @@ protected void write(Schema schema, Object datum, Encoder out) throws IOExceptio case ENUM: super.writeWithoutConversion(schema, GENERIC_DATA.createEnum(datum.toString(), schema), out); return; + case FIXED: + case BYTES: + super.writeWithoutConversion(schema, datum, out); + return; case INT: if (datum.getClass() == CLS_STRING) { String str = (String) datum; diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/TestLogicalTypes.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/TestLogicalTypes.java new file mode 100644 index 000000000..faaac0ca9 --- /dev/null +++ b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/TestLogicalTypes.java @@ -0,0 +1,170 @@ +package com.fasterxml.jackson.dataformat.avro.schema; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.dataformat.avro.AvroType; +import com.fasterxml.jackson.dataformat.avro.AvroMapper; +import com.fasterxml.jackson.dataformat.avro.AvroSchema; +import com.fasterxml.jackson.dataformat.avro.AvroTestBase; +import org.apache.avro.Schema; +import org.apache.avro.SchemaParseException; +import org.junit.Assert; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.UUID; + +public class TestLogicalTypes extends AvroTestBase { + + static class BytesDecimalType { + @JsonProperty(required = true) + @AvroType(schemaType = Schema.Type.BYTES, logicalType = AvroType.LogicalType.DECIMAL, precision = 5) + public BigDecimal value; + } + + static class FixedNoNameDecimalType { + @JsonProperty(required = true) + @AvroType(precision = 5, schemaType = Schema.Type.FIXED, logicalType = AvroType.LogicalType.DECIMAL) + public BigDecimal value; + } + + static class FixedDecimalType { + @JsonProperty(required = true) + @AvroType(precision = 5, + schemaType = Schema.Type.FIXED, + typeName = "foo", + typeNamespace = "com.fasterxml.jackson.dataformat.avro.schema", + fixedSize = 8, + logicalType = AvroType.LogicalType.DECIMAL + ) + public BigDecimal value; + } + + static class TimestampMillisecondsType { + @AvroType(schemaType = Schema.Type.LONG, logicalType = AvroType.LogicalType.TIMESTAMP_MILLISECOND) + @JsonProperty(required = true) + public Date value; + } + + static class TimeMillisecondsType { + @AvroType(schemaType = Schema.Type.INT, logicalType = AvroType.LogicalType.TIME_MILLISECOND) + @JsonProperty(required = true) + public Date value; + } + + static class TimestampMicrosecondsType { + @AvroType(schemaType = Schema.Type.LONG, logicalType = AvroType.LogicalType.TIMESTAMP_MICROSECOND) + @JsonProperty(required = true) + public Date value; + } + + static class TimeMicrosecondsType { + @AvroType(schemaType = Schema.Type.LONG, logicalType = AvroType.LogicalType.TIME_MICROSECOND) + @JsonProperty(required = true) + public Date value; + } + + static class DateType { + @AvroType(schemaType = Schema.Type.INT, logicalType = AvroType.LogicalType.DATE) + @JsonProperty(required = true) + public Date value; + } + + static class UUIDType { + @AvroType(schemaType = Schema.Type.STRING, logicalType = AvroType.LogicalType.UUID) + @JsonProperty(required = true) + public UUID value; + } + + AvroSchema getSchema(Class cls) throws JsonMappingException { + AvroMapper avroMapper = new AvroMapper(); + AvroSchemaGenerator avroSchemaGenerator = new AvroSchemaGenerator(); + avroMapper.acceptJsonFormatVisitor(cls, avroSchemaGenerator); + AvroSchema schema = avroSchemaGenerator.getGeneratedSchema(); + assertNotNull("Schema should not be null.", schema); + assertEquals(Schema.Type.RECORD, schema.getAvroSchema().getType()); + System.out.println(schema.getAvroSchema().toString(true)); + return schema; + } + + void assertLogicalType(Schema.Field field, final Schema.Type type, final String logicalType) { + assertEquals("schema().getType() does not match.", type, field.schema().getType()); + assertNotNull("logicalType should not be null.", field.schema().getLogicalType()); + assertEquals("logicalType does not match.", logicalType, field.schema().getLogicalType().getName()); + field.schema().getLogicalType().validate(field.schema()); + } + + public void testFixedNoNameDecimalType() throws JsonMappingException { + try { + AvroSchema avroSchema = getSchema(FixedNoNameDecimalType.class); + Schema schema = avroSchema.getAvroSchema(); + Schema.Field field = schema.getField("value"); + assertLogicalType(field, Schema.Type.BYTES, "decimal"); + assertEquals(5, field.schema().getObjectProp("precision")); + assertEquals(0, field.schema().getObjectProp("scale")); + Assert.fail("SchemaParseException should have been thrown"); + } catch (SchemaParseException ex) { + + } + } + + public void testBytesDecimalType() throws JsonMappingException { + AvroSchema avroSchema = getSchema(BytesDecimalType.class); + Schema schema = avroSchema.getAvroSchema(); + Schema.Field field = schema.getField("value"); + assertLogicalType(field, Schema.Type.BYTES, "decimal"); + assertEquals(5, field.schema().getObjectProp("precision")); + assertEquals(0, field.schema().getObjectProp("scale")); + } + + public void testFixedDecimalType() throws JsonMappingException { + AvroSchema avroSchema = getSchema(FixedDecimalType.class); + Schema schema = avroSchema.getAvroSchema(); + Schema.Field field = schema.getField("value"); + assertLogicalType(field, Schema.Type.FIXED, "decimal"); + assertEquals(5, field.schema().getObjectProp("precision")); + assertEquals(0, field.schema().getObjectProp("scale")); + } + + public void testTimestampMillisecondsType() throws JsonMappingException { + AvroSchema avroSchema = getSchema(TimestampMillisecondsType.class); + Schema schema = avroSchema.getAvroSchema(); + Schema.Field field = schema.getField("value"); + assertLogicalType(field, Schema.Type.LONG, "timestamp-millis"); + } + + public void testTimeMillisecondsType() throws JsonMappingException { + AvroSchema avroSchema = getSchema(TimeMillisecondsType.class); + Schema schema = avroSchema.getAvroSchema(); + Schema.Field field = schema.getField("value"); + assertLogicalType(field, Schema.Type.INT, "time-millis"); + } + + public void testTimestampMicrosecondsType() throws JsonMappingException { + AvroSchema avroSchema = getSchema(TimestampMicrosecondsType.class); + Schema schema = avroSchema.getAvroSchema(); + Schema.Field field = schema.getField("value"); + assertLogicalType(field, Schema.Type.LONG, "timestamp-micros"); + } + + public void testTimeMicrosecondsType() throws JsonMappingException { + AvroSchema avroSchema = getSchema(TimeMicrosecondsType.class); + Schema schema = avroSchema.getAvroSchema(); + Schema.Field field = schema.getField("value"); + assertLogicalType(field, Schema.Type.LONG, "time-micros"); + } + + public void testDateType() throws JsonMappingException { + AvroSchema avroSchema = getSchema(DateType.class); + Schema schema = avroSchema.getAvroSchema(); + Schema.Field field = schema.getField("value"); + assertLogicalType(field, Schema.Type.INT, "date"); + } + + public void testUUIDType() throws JsonMappingException { + AvroSchema avroSchema = getSchema(UUIDType.class); + Schema schema = avroSchema.getAvroSchema(); + Schema.Field field = schema.getField("value"); + assertLogicalType(field, Schema.Type.STRING, "uuid"); + } +} diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/TestSimpleGeneration.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/TestSimpleGeneration.java index 84948f64f..61c96cff8 100644 --- a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/TestSimpleGeneration.java +++ b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/schema/TestSimpleGeneration.java @@ -31,6 +31,27 @@ static class WithDate { public Date date; } + static class WithAvroTypeFixed { + @JsonProperty(required = true) + @AvroType(typeName = "FixedFieldBytes", fixedSize = 4, schemaType = Schema.Type.FIXED) + public byte[] fixedField; + + @JsonProperty(value = "wff", required = true) + @AvroType(typeName = "WrappedFixedFieldBytes", fixedSize = 8, schemaType = Schema.Type.FIXED) + public WithFixedField.WrappedByteArray wrappedFixedField; + + void setValue(byte[] bytes) { + this.fixedField = bytes; + } + + static class WrappedByteArray { + @JsonValue + public ByteBuffer getBytes() { + return null; + } + } + } + static class WithFixedField { @JsonProperty(required = true) @AvroFixedSize(typeName = "FixedFieldBytes", size = 4) @@ -158,6 +179,19 @@ public void testFixed() throws Exception assertEquals(8, wrappedFieldSchema.getFixedSize()); } + public void testFixedAvroType() throws Exception + { + AvroSchemaGenerator gen = new AvroSchemaGenerator(); + MAPPER.acceptJsonFormatVisitor(WithAvroTypeFixed.class, gen); + Schema generated = gen.getAvroSchema(); + Schema fixedFieldSchema = generated.getField("fixedField").schema(); + assertEquals(Schema.Type.FIXED, fixedFieldSchema.getType()); + assertEquals(4, fixedFieldSchema.getFixedSize()); + + Schema wrappedFieldSchema = generated.getField("wff").schema(); + assertEquals(Schema.Type.FIXED, wrappedFieldSchema.getType()); + assertEquals(8, wrappedFieldSchema.getFixedSize()); + } // as per [dataformats-binary#98], no can do (unless we start supporting polymorphic // handling or something...) public void testSchemaForUntypedMap() throws Exception diff --git a/pom.xml b/pom.xml index 819a5f7c1..93105f013 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,7 @@ cbor smile avro + avro-java8 protobuf ion