Skip to content

Commit

Permalink
Support to serialize and de-serialize java.time types into Avro type …
Browse files Browse the repository at this point in the history
…and logicalType.
  • Loading branch information
MichalFoksa committed Jun 8, 2021
1 parent 62570b4 commit 1bd02ba
Show file tree
Hide file tree
Showing 11 changed files with 773 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.fasterxml.jackson.dataformat.avro.jsr310;

import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.core.json.PackageVersion;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.dataformat.avro.jsr310.deser.AvroInstantDeserializer;
import com.fasterxml.jackson.dataformat.avro.jsr310.deser.AvroLocalDateDeserializer;
import com.fasterxml.jackson.dataformat.avro.jsr310.deser.AvroLocalDateTimeDeserializer;
import com.fasterxml.jackson.dataformat.avro.jsr310.deser.AvroLocalTimeDeserializer;
import com.fasterxml.jackson.dataformat.avro.jsr310.ser.AvroInstantSerializer;
import com.fasterxml.jackson.dataformat.avro.jsr310.ser.AvroLocalDateSerializer;
import com.fasterxml.jackson.dataformat.avro.jsr310.ser.AvroLocalDateTimeSerializer;
import com.fasterxml.jackson.dataformat.avro.jsr310.ser.AvroLocalTimeSerializer;

import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;

/**
* A module that installs a collection of serializers and deserializers for java.time classes.
*/
public class AvroJavaTimeModule extends SimpleModule {

private static final long serialVersionUID = 1L;

public AvroJavaTimeModule() {
super(PackageVersion.VERSION);

addSerializer(Instant.class, AvroInstantSerializer.INSTANT);
addSerializer(OffsetDateTime.class, AvroInstantSerializer.OFFSET_DATE_TIME);
addSerializer(ZonedDateTime.class, AvroInstantSerializer.ZONED_DATE_TIME);
addSerializer(LocalDateTime.class, AvroLocalDateTimeSerializer.INSTANCE);
addSerializer(LocalDate.class, AvroLocalDateSerializer.INSTANCE);
addSerializer(LocalTime.class, AvroLocalTimeSerializer.INSTANCE);

addDeserializer(Instant.class, AvroInstantDeserializer.INSTANT);
addDeserializer(OffsetDateTime.class, AvroInstantDeserializer.OFFSET_DATE_TIME);
addDeserializer(ZonedDateTime.class, AvroInstantDeserializer.ZONED_DATE_TIME);
addDeserializer(LocalDateTime.class, AvroLocalDateTimeDeserializer.INSTANCE);
addDeserializer(LocalDate.class, AvroLocalDateDeserializer.INSTANCE);
addDeserializer(LocalTime.class, AvroLocalTimeDeserializer.INSTANCE);
}

@Override
public String getModuleName() {
return getClass().getName();
}

@Override
public Version version() {
return PackageVersion.VERSION;
}

@Override
public void setupModule(SetupContext context) {
super.setupModule(context);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.fasterxml.jackson.dataformat.avro.jsr310.deser;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer;

import java.io.IOException;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.Temporal;
import java.util.function.BiFunction;

/**
* Deserializer for variants of java.time classes (Instant, OffsetDateTime, ZonedDateTime) from an integer value.
*
* Deserialized value represents an instant on the global timeline, independent of a particular time zone or
* calendar, with a precision of one millisecond from the unix epoch, 1 January 1970 00:00:00.000 UTC.
* Time zone information is lost at serialization. Time zone data types receives time zone from deserialization context.
*
* Deserialization from string is not supported.
*
* @param <T> The type of a instant class that can be deserialized.
*/
public class AvroInstantDeserializer<T extends Temporal> extends StdScalarDeserializer<T> {

private static final long serialVersionUID = 1L;

public static final AvroInstantDeserializer<Instant> INSTANT =
new AvroInstantDeserializer<>(Instant.class, (instant, zoneID) -> instant);

public static final AvroInstantDeserializer<OffsetDateTime> OFFSET_DATE_TIME =
new AvroInstantDeserializer<>(OffsetDateTime.class, OffsetDateTime::ofInstant);

public static final AvroInstantDeserializer<ZonedDateTime> ZONED_DATE_TIME =
new AvroInstantDeserializer<>(ZonedDateTime.class, ZonedDateTime::ofInstant);

protected final BiFunction<Instant, ZoneId, T> fromInstant;

protected AvroInstantDeserializer(Class<T> t, BiFunction<Instant, ZoneId, T> fromInstant) {
super(t);
this.fromInstant = fromInstant;
}

@SuppressWarnings("unchecked")
@Override
public T deserialize(JsonParser p, DeserializationContext context) throws IOException, JsonProcessingException {
final ZoneId defaultZoneId = context.getTimeZone().toZoneId().normalized();
switch (p.getCurrentToken()) {
case VALUE_NUMBER_INT:
return fromLong(p.getLongValue(), defaultZoneId);
default:
try {
return (T) context.handleUnexpectedToken(_valueClass, p);
} catch (JsonMappingException e) {
throw e;
} catch (IOException e) {
throw JsonMappingException.fromUnexpectedIOE(e);
}
}
}

private T fromLong(long longValue, ZoneId defaultZoneId) {
/**
* Number of milliseconds, independent of a particular time zone or calendar,
* from 1 January 1970 00:00:00.000 UTC.
*/
return fromInstant.apply(Instant.ofEpochMilli(longValue), defaultZoneId);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.fasterxml.jackson.dataformat.avro.jsr310.deser;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer;

import java.io.IOException;
import java.time.LocalDate;

/**
* Deserializer for {@link LocalDate} from and integer value.
*
* Deserialized value represents number of days from the unix epoch, 1 January 1970.
*
* Deserialization from string is not supported.
*/
public class AvroLocalDateDeserializer extends StdScalarDeserializer<LocalDate> {

private static final long serialVersionUID = 1L;

public static final AvroLocalDateDeserializer INSTANCE = new AvroLocalDateDeserializer();

protected AvroLocalDateDeserializer() {
super(LocalDate.class);
}

@SuppressWarnings("unchecked")
@Override
public LocalDate deserialize(JsonParser p, DeserializationContext context) throws IOException, JsonProcessingException {
switch (p.getCurrentToken()) {
case VALUE_NUMBER_INT:
return fromLong(p.getLongValue());
default:
try {
return (LocalDate) context.handleUnexpectedToken(_valueClass, p);
} catch (JsonMappingException e) {
throw e;
} catch (IOException e) {
throw JsonMappingException.fromUnexpectedIOE(e);
}
}
}

private LocalDate fromLong(long longValue) {
/**
* Number of days from the unix epoch, 1 January 1970..
*/
return LocalDate.ofEpochDay(longValue);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.fasterxml.jackson.dataformat.avro.jsr310.deser;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer;

import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;

/**
* Deserializer for {@link LocalDateTime} from an integer value.
*
* Deserialized value represents timestamp in a local timezone, regardless of what specific time zone
* is considered local, with a precision of one millisecond from 1 January 1970 00:00:00.000.
*
* Deserialization from string is not supported.
*/
public class AvroLocalDateTimeDeserializer extends StdScalarDeserializer<LocalDateTime> {

private static final long serialVersionUID = 1L;

public static final AvroLocalDateTimeDeserializer INSTANCE = new AvroLocalDateTimeDeserializer();

protected AvroLocalDateTimeDeserializer() {
super(LocalDateTime.class);
}

@SuppressWarnings("unchecked")
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext context) throws IOException, JsonProcessingException {
switch (p.getCurrentToken()) {
case VALUE_NUMBER_INT:
return fromLong(p.getLongValue());
default:
try {
return (LocalDateTime) context.handleUnexpectedToken(_valueClass, p);
} catch (JsonMappingException e) {
throw e;
} catch (IOException e) {
throw JsonMappingException.fromUnexpectedIOE(e);
}
}
}

private LocalDateTime fromLong(long longValue) {
/**
* Number of milliseconds in a local timezone, regardless of what specific time zone is considered local,
* from 1 January 1970 00:00:00.000.
*/
return LocalDateTime.ofInstant(Instant.ofEpochMilli(longValue), ZoneOffset.ofTotalSeconds(0));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.fasterxml.jackson.dataformat.avro.jsr310.deser;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.LocalTime;

/**
* Deserializer for {@link LocalTime} from an integer value.
*
* Deserialized value represents time of day, with no reference to a particular calendar,
* time zone or date, where the int stores the number of milliseconds after midnight, 00:00:00.000.
*
* Deserialization from string is not supported.
*/
public class AvroLocalTimeDeserializer extends StdScalarDeserializer<LocalTime> {

private static final long serialVersionUID = 1L;

public static final AvroLocalTimeDeserializer INSTANCE = new AvroLocalTimeDeserializer();

protected AvroLocalTimeDeserializer() {
super(LocalDateTime.class);
}

@SuppressWarnings("unchecked")
@Override
public LocalTime deserialize(JsonParser p, DeserializationContext context) throws IOException, JsonProcessingException {
switch (p.getCurrentToken()) {
case VALUE_NUMBER_INT:
return fromLong(p.getLongValue());
default:
try {
return (LocalTime) context.handleUnexpectedToken(_valueClass, p);
} catch (JsonMappingException e) {
throw e;
} catch (IOException e) {
throw JsonMappingException.fromUnexpectedIOE(e);
}
}
}

private LocalTime fromLong(long longValue) {
/**
* Number of milliseconds, with no reference to a particular calendar, time zone or date, after
* midnight, 00:00:00.000.
*/
return LocalTime.ofNanoOfDay(longValue * 1000_000L);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.fasterxml.jackson.dataformat.avro.jsr310.ser;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonIntegerFormatVisitor;
import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer;

import java.io.IOException;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.time.temporal.Temporal;
import java.util.function.Function;

/**
* Serializer for variants of java.time classes (Instant, OffsetDateTime, ZonedDateTime) into long value.
*
* Serialized value represents an instant on the global timeline, independent of a particular time zone or
* calendar, with a precision of one millisecond from the unix epoch, 1 January 1970 00:00:00.000 UTC.
* Please note that time zone information gets lost in this process. Upon reading a value back, we can only
* reconstruct the instant, but not the original representation.
*
* Note: In combination with {@link com.fasterxml.jackson.dataformat.avro.schema.DateTimeVisitor} it aims to produce
* Avro schema with type long with logicalType timestamp-millis:
* {
* "type" : "long",
* "logicalType" : "timestamp-millis"
* }
*
* {@link AvroInstantSerializer} does not support serialization to string.
*
* @param <T> The type of a instant class that can be serialized.
*/
public class AvroInstantSerializer<T extends Temporal> extends StdScalarSerializer<T> {

private static final long serialVersionUID = 1L;

public static final AvroInstantSerializer<Instant> INSTANT =
new AvroInstantSerializer<>(Instant.class, Function.identity());

public static final AvroInstantSerializer<OffsetDateTime> OFFSET_DATE_TIME =
new AvroInstantSerializer<>(OffsetDateTime.class, OffsetDateTime::toInstant);

public static final AvroInstantSerializer<ZonedDateTime> ZONED_DATE_TIME =
new AvroInstantSerializer<>(ZonedDateTime.class, ZonedDateTime::toInstant);

private final Function<T, Instant> getInstant;

protected AvroInstantSerializer(Class<T> t, Function<T, Instant> getInstant) {
super(t);
this.getInstant = getInstant;
}

@Override
public void serialize(T value, JsonGenerator gen, SerializerProvider provider) throws IOException {
/**
* Number of milliseconds, independent of a particular time zone or calendar,
* from 1 January 1970 00:00:00.000 UTC.
*/
final Instant instant = getInstant.apply(value);
gen.writeNumber(instant.toEpochMilli());
}

@Override
public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException {
JsonIntegerFormatVisitor v2 = visitor.expectIntegerFormat(typeHint);
if (v2 != null) {
v2.numberType(JsonParser.NumberType.LONG);
}
}

}
Loading

0 comments on commit 1bd02ba

Please sign in to comment.