Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.fasterxml.jackson.jr.extension.javatime;

import java.io.IOException;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalQuery;
import java.util.Objects;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.jr.ob.api.ValueReader;
import com.fasterxml.jackson.jr.ob.impl.JSONReader;

/**
* {@link ValueReader} designed to easily handle {@link TemporalAccessor} descendants such as
* {@link OffsetDateTime} and {@link ZonedDateTime}. Their string representation is expected to
* be in ISO 8601 format.
* @since 2.20
* @see <a href="https://en.wikipedia.org/wiki/ISO_8601">ISO 8601 on Wikipedia</a>
*/
public class DefaultDateTimeValueReader<T extends TemporalAccessor> extends ValueReader {
private final TemporalQuery<T> _query;

/**
* Constructor that includes a temportal query that is to be used during formatting.
* @param targetType Target type
* @param query Temporal query for parsing
*/
public DefaultDateTimeValueReader(Class<T> targetType, TemporalQuery<T> query) {
super(targetType);

this._query = Objects.requireNonNull(query);
}

@Override
public Object read(JSONReader reader, JsonParser p) throws IOException {
// SimpleValueReader allows 'Date' objects to be null, so this should probably
// also be the case here.
if (p.hasToken(JsonToken.VALUE_NULL)) {
return null;
}

return JavaTimeReaderWriterProvider.FORMATTER.parse(p.getText(), _query);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
public class JacksonJrJavaTimeExtension extends JacksonJrExtension {
final static JavaTimeReaderWriterProvider DEFAULT_RW_PROVIDER = new JavaTimeReaderWriterProvider();

private JavaTimeReaderWriterProvider readerWriterProvider = DEFAULT_RW_PROVIDER;
private JavaTimeReaderWriterProvider _readerWriterProvider = DEFAULT_RW_PROVIDER;

@Override
protected void register(ExtensionContext ctxt) {
ctxt.insertProvider(readerWriterProvider);
ctxt.insertProvider(_readerWriterProvider);
}

public JacksonJrJavaTimeExtension with(JavaTimeReaderWriterProvider p) {
readerWriterProvider = p;
_readerWriterProvider = p;
return this;
}
}
Original file line number Diff line number Diff line change
@@ -1,48 +1,106 @@
package com.fasterxml.jackson.jr.extension.javatime;

import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;

import com.fasterxml.jackson.jr.ob.api.ReaderWriterProvider;
import com.fasterxml.jackson.jr.ob.api.ValueReader;
import com.fasterxml.jackson.jr.ob.api.ValueWriter;
import com.fasterxml.jackson.jr.ob.impl.JSONReader;
import com.fasterxml.jackson.jr.ob.impl.JSONWriter;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
* Provider for {@link ValueReader}s and {@link ValueWriter}s for Date/Time
* types supported by Java Time Extension.
*/
public class JavaTimeReaderWriterProvider extends ReaderWriterProvider
{
private DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;

public JavaTimeReaderWriterProvider() { }
private ZoneId _fallbackLocalZoneId;

protected static final DateTimeFormatter FORMATTER = createFormatter(true);
protected static final DateTimeFormatter LOCAL_FORMATTER = createFormatter(false);

public JavaTimeReaderWriterProvider() {
_fallbackLocalZoneId = ZoneId.systemDefault();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it is different from standard Jackson defaulting which for time zones/offsets tries to use global "neutral" default (UTC where possible, GMT otherwise), to avoid relying on local defaults that are arbitrary.

But I am not sure such choice exists for ZoneId?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand what you mean. I personally think the default for a LocalDateTime object should probably always be in the local time for the user. Which this probably comes closest to. But it wouldn't hurt setting it to UTC to be safe, considering the user can change it.

I have been doubting another time zone related segment myself, which is the includeUtcDefault boolean that occurs inside of JavaTimeReaderWriterProvider. Because that segment defaults ZonedDateTime and OffsetDateTimes to UTC when no time zone is included in its ISO 8601 string. I was thinking that maybe it would make more sense to default to the time zone that the user configured. But I am sort of undecided on that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought this over. Wikipedia says the following:

If no UTC relation information is given with a time representation, the time is assumed to be in local time.

So I suggest I'll change the formatter to use the user's selected time zone for dates that do not contain offset information. That will automatically mean the end of the static formatter.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmh. Ok.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The static formatters are still here. I found it to be really difficult to apply offsets automatically from the DateTimeFormatter. I did change the static method to be private though.

}

@Override
public ValueReader findValueReader(JSONReader readContext, Class<?> type) {
return LocalDateTime.class.isAssignableFrom(type) ? new LocalDateTimeValueReader(dateTimeFormatter) : null;
if (LocalDateTime.class.isAssignableFrom(type)) {
return new LocalDateTimeValueReader(_fallbackLocalZoneId);
}
if (OffsetDateTime.class.isAssignableFrom(type)) {
return new DefaultDateTimeValueReader<OffsetDateTime>(OffsetDateTime.class, OffsetDateTime::from);
}
if (ZonedDateTime.class.isAssignableFrom(type)) {
return new DefaultDateTimeValueReader<ZonedDateTime>(ZonedDateTime.class, ZonedDateTime::from);
}
return null;
}

@Override
public ValueWriter findValueWriter(JSONWriter writeContext, Class<?> type) {
return LocalDateTime.class.isAssignableFrom(type) ? new LocalDateTimeValueWriter(dateTimeFormatter) : null;
if (LocalDateTime.class.isAssignableFrom(type)) {
return new LocalDateTimeValueWriter();
}
if (OffsetDateTime.class.isAssignableFrom(type)) {
return new OffsetDateTimeValueWriter();
}
if (ZonedDateTime.class.isAssignableFrom(type)) {
return new ZonedDateTimeValueWriter();
}
return null;
}

/**
* Method for reconfiguring {@link DateTimeFormatter} used for reading/writing
* following Date/Time value types:
*<ul>
* <li>{@code java.time.LocalDateTime}
* </li>
*</ul>
*
* @param formatter
*
* @return This provider instance for call chaining
* Setter to configure a time zone that is to be applied when a zoned ISO 8601 date time needs
* to be converted to a <code>LocalDateTime</code>. Can be set to <code>null</code> to apply
* the system default.
* @see java.time.LocalDateTime
* @param fallbackLocalZoneId Time zone to apply, or <code>null</code>
* @since 2.20
* @return Reference for chaining
*/
public JavaTimeReaderWriterProvider withDateTimeFormatter(DateTimeFormatter formatter) {
dateTimeFormatter = formatter;
public JavaTimeReaderWriterProvider setLocalFallbackTimeZone(ZoneId fallbackLocalZoneId) {
_fallbackLocalZoneId = fallbackLocalZoneId == null ? ZoneId.systemDefault() : fallbackLocalZoneId;
return this;
}

/**
* Create a forgiving date time formatter that allows different interpretations of ISO 8601
* strings to be parsed.
*
* @param includeUtcDefault Set to <code>true</code> to set UTC to be the default offset
* for non-local date times. Set to <code>false</code> when handling
* local date times.
* @since 2.20
* @return Formatter
*/
public static DateTimeFormatter createFormatter(boolean includeUtcDefault) {
final DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder()
.parseLenient()
.appendPattern("yyyy-MM-dd'T'HH:mm:ss")
.optionalStart()
.appendFraction(ChronoField.MILLI_OF_SECOND, 1, 9, true)
.optionalEnd()
.optionalStart()
.appendOffsetId()
.optionalStart()
.appendLiteral('[')
.appendZoneRegionId()
.appendLiteral(']')
.optionalEnd()
.optionalEnd();

if (includeUtcDefault) {
builder.parseDefaulting(ChronoField.OFFSET_SECONDS, 0);
}

return builder.toFormatter();
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,60 @@
package com.fasterxml.jackson.jr.extension.javatime;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.TemporalAccessor;
import java.util.Objects;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.jr.ob.api.ValueReader;
import com.fasterxml.jackson.jr.ob.impl.JSONReader;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
* {@link ValueReader} designed specifically to handle {@link LocalDateTime} instances. This
* requires a slightly different approach than other date time types because we want to be as
* forgiving as possible and be able to also interpret ISO 8601 dates that include an offset
* or zone ID.
* @since 2.20
* @see <a href="https://en.wikipedia.org/wiki/ISO_8601">ISO 8601 on Wikipedia</a>
*/
public class LocalDateTimeValueReader extends ValueReader {
private final DateTimeFormatter formatter;

public LocalDateTimeValueReader(DateTimeFormatter formatter) {
private final ZoneId _localZoneId;

/**
* Constructor that accepts a zone ID that should be used to when a ISO 8601 string that
* includes an offset needs to be converted to a local date time.
* @param localZoneId Destination zone ID
*/
public LocalDateTimeValueReader(ZoneId localZoneId) {
super(LocalDateTime.class);
this.formatter = formatter;
_localZoneId = Objects.requireNonNull(localZoneId);
}

@Override
public Object read(JSONReader reader, JsonParser p) throws IOException {
return LocalDateTime.parse(p.getText(), formatter);
// SimpleValueReader allows 'Date' objects to be null, so this should probably
// also be the case here.
if (p.hasToken(JsonToken.VALUE_NULL)) {
return null;
}

final TemporalAccessor ta = JavaTimeReaderWriterProvider.LOCAL_FORMATTER.parseBest(p.getText(),
ZonedDateTime::from, LocalDateTime::from);

if (ta instanceof ZonedDateTime) {
// Convert a date time that unexpectedly includes a time offset or zone ID, to a proper local date time
return ((ZonedDateTime)ta).withZoneSameInstant(_localZoneId).toLocalDateTime();
}
if (ta instanceof LocalDateTime) {
return ta;
}

throw new IOException(String.format("Converting \"%s\" to an instance of %s was "
+ "unexpected and should not occur",
p.getText(),
ta.getClass().getSimpleName()));
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
package com.fasterxml.jackson.jr.extension.javatime;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.jr.ob.api.ValueWriter;
import com.fasterxml.jackson.jr.ob.impl.JSONWriter;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class LocalDateTimeValueWriter implements ValueWriter {
private final DateTimeFormatter formatter;

public LocalDateTimeValueWriter(DateTimeFormatter formatter) {
this.formatter = formatter;
}
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.jr.ob.api.ValueWriter;
import com.fasterxml.jackson.jr.ob.impl.JSONWriter;

/**
* {@link ValueWriter} that converts a {@link LocalDateTime} to an ISO 8601 string without
* an offset or zone ID.
* @see <a href="https://en.wikipedia.org/wiki/ISO_8601">ISO 8601 on Wikipedia</a>
*/
public class LocalDateTimeValueWriter implements ValueWriter {
@Override
public void writeValue(JSONWriter context, JsonGenerator g, Object value) throws IOException {
String localDateTimeString = ((LocalDateTime) value).format(formatter);
final String localDateTimeString = ((LocalDateTime) value).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
context.writeValue(localDateTimeString);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.fasterxml.jackson.jr.extension.javatime;

import java.io.IOException;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.jr.ob.api.ValueWriter;
import com.fasterxml.jackson.jr.ob.impl.JSONWriter;

/**
* {@link ValueWriter} that converts a {@link OffsetDateTime} to an ISO 8601 string including
* an offset.
* @see <a href="https://en.wikipedia.org/wiki/ISO_8601">ISO 8601 on Wikipedia</a>
* @since 2.20
*/
public class OffsetDateTimeValueWriter implements ValueWriter {
@Override
public void writeValue(JSONWriter context, JsonGenerator g, Object value) throws IOException {
String offsetDateTimeString = ((OffsetDateTime) value).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
context.writeValue(offsetDateTimeString);
}

@Override
public Class<?> valueType() {
return OffsetDateTime.class;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.fasterxml.jackson.jr.extension.javatime;

import java.io.IOException;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.jr.ob.api.ValueWriter;
import com.fasterxml.jackson.jr.ob.impl.JSONWriter;

/**
* {@link ValueWriter} that converts a {@link ZonedDateTime} to an ISO 8601 string including
* an offset and a zone ID.
* @see <a href="https://en.wikipedia.org/wiki/ISO_8601">ISO 8601 on Wikipedia</a>
* @since 2.20
*/
public class ZonedDateTimeValueWriter implements ValueWriter {
@Override
public void writeValue(JSONWriter context, JsonGenerator g, Object value) throws IOException {
String zonedDateTimeString = ((ZonedDateTime) value).format(DateTimeFormatter.ISO_ZONED_DATE_TIME);
context.writeValue(zonedDateTimeString);
}

@Override
public Class<?> valueType() {
return ZonedDateTime.class;
}
}
Loading