Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions ion/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ byte[] encoded = mapper.writeValueAsBytes(value);
SomeType otherValue = mapper.readValue(data, SomeType.class);
```

### java.time JSR 310
There is support for (de)serializing some `java.time` classes directly from/to Ion timestamp values.

```java
IonObjectMapper mapper = IonObjectMapper.builder()
.addModule(new IonJavaTimeModule())
//Disable writing dates as numeric timestamp values to allow writing as Ion timestamp values.
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.build();
```

## Documentation

See [Wiki](../../../wiki) (includes Javadocs)
2 changes: 2 additions & 0 deletions ion/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ tree model)
<properties>
<packageVersion.dir>com/fasterxml/jackson/dataformat/ion</packageVersion.dir>
<packageVersion.package>${project.groupId}.ion</packageVersion.package>
<javac.src.version>1.8</javac.src.version>
<javac.target.version>1.8</javac.target.version>
</properties>

<dependencies>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.fasterxml.jackson.dataformat.ion.jsr310;

import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;

import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.core.json.PackageVersion;
import com.fasterxml.jackson.databind.module.SimpleModule;

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

private static final long serialVersionUID = 1L;

public IonJavaTimeModule() {
super(PackageVersion.VERSION);
addSerializer(Instant.class, IonTimestampInstantSerializer.INSTANT);
addSerializer(OffsetDateTime.class, IonTimestampInstantSerializer.OFFSET_DATE_TIME);
addSerializer(ZonedDateTime.class, IonTimestampInstantSerializer.ZONED_DATE_TIME);

addDeserializer(Instant.class, IonTimestampInstantDeserializer.INSTANT);
addDeserializer(OffsetDateTime.class, IonTimestampInstantDeserializer.OFFSET_DATE_TIME);
addDeserializer(ZonedDateTime.class, IonTimestampInstantDeserializer.ZONED_DATE_TIME);
}

@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,132 @@
package com.fasterxml.jackson.dataformat.ion.jsr310;

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

import com.amazon.ion.Timestamp;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Feature;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer;

/**
* A deserializer for variants of java.time classes that represent a specific instant on the timeline
* (Instant, OffsetDateTime, ZonedDateTime) which supports deserializing from an Ion timestamp value.
*
* @param <T> The type of a instant class that can be deserialized.
*/
public class IonTimestampInstantDeserializer<T extends Temporal> extends StdScalarDeserializer<T>
implements ContextualDeserializer {

private static final long serialVersionUID = 1L;

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

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

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

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

/**
* Flag for <code>JsonFormat.Feature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE</code>
*/
protected final Boolean adjustToContextTZOverride;

protected IonTimestampInstantDeserializer(Class<T> vc, BiFunction<Instant, ZoneId, T> fromInstant) {
super(vc);
this.fromInstant = fromInstant;
this.adjustToContextTZOverride = null;
}

protected IonTimestampInstantDeserializer(IonTimestampInstantDeserializer<T> base,
Boolean adjustToContextTZOverride) {

super(base.handledType());
this.fromInstant = base.fromInstant;
this.adjustToContextTZOverride = adjustToContextTZOverride;
}

@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_FLOAT:
return fromDecimal(p.getDecimalValue(), defaultZoneId);
case VALUE_NUMBER_INT:
return fromLong(p.getLongValue(), defaultZoneId, context);
case VALUE_EMBEDDED_OBJECT:
final Object embeddedObject = p.getEmbeddedObject();
if (Timestamp.class.isAssignableFrom(embeddedObject.getClass())) {
return fromTimestamp((Timestamp)embeddedObject, defaultZoneId);
}
default:
try {
return (T) context.handleUnexpectedToken(_valueClass, p);
} catch (JsonMappingException e) {
throw e;
} catch (IOException e) {
throw JsonMappingException.fromUnexpectedIOE(e);
}
}
}

@Override
public JsonDeserializer<T> createContextual(DeserializationContext ctxt, BeanProperty property)
throws JsonMappingException {

final JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType());
if (format != null) {
return new IonTimestampInstantDeserializer<T>(this,
format.getFeature(Feature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE));
}
return this;
}

private T fromDecimal(BigDecimal decimalValue, ZoneId defaultZoneId) {
final Instant instant = TimestampUtils.fromFractionalSeconds(decimalValue);
return fromInstant.apply(instant, defaultZoneId);
}

private T fromLong(long longValue, ZoneId defaultZoneId, DeserializationContext context) {
if(context.isEnabled(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)){
return fromInstant.apply(Instant.ofEpochSecond(longValue, 0), defaultZoneId);
}
return fromInstant.apply(Instant.ofEpochMilli(longValue), defaultZoneId);
}

private T fromTimestamp(Timestamp timestamp, ZoneId defaultZoneId) {
final Instant instant = TimestampUtils.toInstant(timestamp);
final ZoneId zoneId = getZoneId(timestamp, defaultZoneId);
return fromInstant.apply(instant, zoneId);
}

private ZoneId getZoneId(Timestamp timestamp, ZoneId defaultZoneId) {
if (Boolean.TRUE.equals(adjustToContextTZOverride)
|| null == timestamp.getLocalOffset()
|| Instant.class.equals(_valueClass)) {

return defaultZoneId;
}
final int localOffsetMinutes = timestamp.getLocalOffset();
return ZoneOffset.ofTotalSeconds(localOffsetMinutes * 60);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package com.fasterxml.jackson.dataformat.ion.jsr310;

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

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Feature;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer;
import com.fasterxml.jackson.dataformat.ion.IonGenerator;

/**
* A serializer for variants of java.time classes that represent a specific instant on the timeline
* (Instant, OffsetDateTime, ZonedDateTime) which supports serializing to an Ion timestamp value.
*
* @param <T> The type of a instant class that can be serialized.
*/
public class IonTimestampInstantSerializer<T extends Temporal> extends StdScalarSerializer<T>
implements ContextualSerializer {

private static final long serialVersionUID = 1L;

public static final IonTimestampInstantSerializer<Instant> INSTANT =
new IonTimestampInstantSerializer<>(Instant.class,
Function.identity(),
(instant) -> ZoneOffset.UTC,
(instant, zoneId) -> instant.atZone(zoneId).getOffset());

public static final IonTimestampInstantSerializer<OffsetDateTime> OFFSET_DATE_TIME =
new IonTimestampInstantSerializer<>(OffsetDateTime.class,
OffsetDateTime::toInstant,
OffsetDateTime::getOffset,
(offsetDateTime, zoneId) -> offsetDateTime.atZoneSameInstant(zoneId).getOffset());

/**
* A serializer for ZoneDateTime's. NOTE: Ion timestamp values can only represent offset values
* so specific time zone values will be converted to an equivalent offset value.
*/
public static final IonTimestampInstantSerializer<ZonedDateTime> ZONED_DATE_TIME =
new IonTimestampInstantSerializer<>(ZonedDateTime.class,
ZonedDateTime::toInstant,
ZonedDateTime::getOffset,
(zonedDateTime, zoneId) -> zonedDateTime.withZoneSameInstant(zoneId).getOffset());

private final Function<T, Instant> getInstant;
private final Function<T, ZoneOffset> getOffset;
private final BiFunction<T, ZoneId, ZoneOffset> getOffsetAtZoneId;

/**
* ZoneId equivalent of <code>JsonFormat.timezone</code>
*/
private final ZoneId zoneIdOverride;

/**
* Flag for <code>JsonFormat.Feature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS</code>
*/
private final Boolean writeDateTimestampsAsNanosOverride;

protected IonTimestampInstantSerializer(Class<T> t,
Function<T, Instant> getInstant,
Function<T, ZoneOffset> getOffset,
BiFunction<T, ZoneId, ZoneOffset> getOffsetAtZoneId) {

super(t);
this.getInstant = getInstant;
this.getOffset = getOffset;
this.getOffsetAtZoneId = getOffsetAtZoneId;
this.zoneIdOverride = null;
this.writeDateTimestampsAsNanosOverride = null;
}

protected IonTimestampInstantSerializer(IonTimestampInstantSerializer<T> base,
ZoneId zoneIdOverride,
Boolean writeDateTimestampsAsNanosOverride) {

super(base.handledType());
this.getInstant = base.getInstant;
this.getOffset = base.getOffset;
this.getOffsetAtZoneId = base.getOffsetAtZoneId;
this.zoneIdOverride = zoneIdOverride;
this.writeDateTimestampsAsNanosOverride = writeDateTimestampsAsNanosOverride;
}

@Override
public void serialize(T value, JsonGenerator gen, SerializerProvider provider) throws IOException {
final Instant instant = getInstant.apply(value);
if (provider.isEnabled(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)) {
if (shouldWriteTimestampsAsNanos(provider)) {
gen.writeNumber(TimestampUtils.getFractionalSeconds(instant));
} else {
gen.writeNumber(instant.toEpochMilli());
}
} else {
final ZoneOffset offset = getOffset(value);
((IonGenerator)gen).writeValue(TimestampUtils.toTimestamp(instant, offset));
}
}

@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
throws JsonMappingException {

final JsonFormat.Value format = findFormatOverrides(prov, property, handledType());
if (format != null) {
return new IonTimestampInstantSerializer<>(this,
format.getTimeZone() == null ? null : format.getTimeZone().toZoneId(),
format.getFeature(Feature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS));
}
return this;
}

private boolean shouldWriteTimestampsAsNanos(SerializerProvider provider) {
if (Boolean.FALSE.equals(writeDateTimestampsAsNanosOverride)) {
return false;
}
return provider.isEnabled(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
|| Boolean.TRUE.equals(writeDateTimestampsAsNanosOverride);
}

private ZoneOffset getOffset(T value) {
if (null != zoneIdOverride) {
return getOffsetAtZoneId.apply(value, zoneIdOverride);
}
return getOffset.apply(value);
}
}
Loading