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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Project is composed of multiple Maven sub-modules, each corresponding to a jar:
* [jr-retrofit2](../../tree/master/jr-retrofit2) contains `jackson-jr` - based handlers for [Retrofit 2](https://square.github.io/retrofit/) library
* Depends on `jackson-jr` and `Retrofit` API jars, and indirectly on `jackson-core`
* [jr-annotation-support](../../tree/master/jr-annotation-support) contains extension with support for a subset of core [Jackson annotations](../../../jackson-annotations)
* [jr-extension-javatime](../../tree/master/jr-extension-javatime) contains extension with support for a subset of Java 8 Date/Time types (e.g. `java.time.LocalDateTime`)
* [jr-extension-javatime](../../tree/master/jr-extension-javatime) contains extension with support for a subset of Java 8 Date/Time types (e.g. `java.time.LocalDateTime`, `java.time.OffsetDateTime` (2.20), `java.time.ZonedDateTime` (2.20))
* jr-all creates an "uber-jar" that contains individual modules along with all their dependencies:
* `jr-objects` classes as-is, without relocating
* `jr-stree` classes as-is, without relocating
Expand Down
7 changes: 5 additions & 2 deletions jr-extension-javatime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This module extends the functionality of jackson-jr by adding support for (a sub

### Status

Added in Jackson 2.17.
Added in Jackson 2.17; extended in 2.20.

### Usage
To be able to use supported annotations, you need to register extension like so:
Expand Down Expand Up @@ -48,7 +48,10 @@ public class MyClass {
### Date Classes currently supported by `JacksonJrJavaTimeExtension`

- `java.time.LocalDateTime`
- `java.time.OffsetDateTime` (2.20+)
- `java.time.ZonedDateTime` (2.20+)

### Plans for Future

- Add support for other Java 8 Date/Time types
- Add support for more Java 8 Date/Time types

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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.JSONObjectException;
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 temporal 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);
_query = Objects.requireNonNull(query);
}

@Override
public Object read(JSONReader reader, JsonParser p) throws IOException {
if (p.hasToken(JsonToken.VALUE_NULL)) {
return null;
}
if (p.hasToken(JsonToken.VALUE_STRING)) {
return JavaTimeReaderWriterProvider.OFFSET_FORMATTER.parse(p.getText(), _query);
}

throw JSONObjectException.from(p,
"Can not create a "+_valueType.getName()+" instance out of "+_tokenDesc(p));
}

/**
* Check if an ISO 8601 string contains a time offset or not.
* @param text ISO 8601 string
* @return Return <code>true</code> when an offset is missing, otherwise: <code>false</code>
*/
protected boolean offsetMissing(String text) {
return !text.matches(".*(Z|[+-]\\d{2}(:?\\d{2})?)$");
}
}
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,117 @@
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 _localZoneId;

protected static final DateTimeFormatter OFFSET_FORMATTER = _createFormatter(true);
protected static final DateTimeFormatter LOCAL_FORMATTER = _createFormatter(false);

public JavaTimeReaderWriterProvider() {
withLocalTimeZone(null);
}

@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(_localZoneId);
}
if (OffsetDateTime.class.isAssignableFrom(type)) {
return new OffsetDateTimeValueReader(_localZoneId);
}
if (ZonedDateTime.class.isAssignableFrom(type)) {
return new ZonedDateTimeValueReader(_localZoneId);
}
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 {@link LocalDateTime}. Can be set to <code>null</code> to apply the
* UTC default.
* @param localZoneId Time zone to apply, or <code>null</code>
* @since 2.20
* @return Reference for chaining
*/
public JavaTimeReaderWriterProvider withDateTimeFormatter(DateTimeFormatter formatter) {
dateTimeFormatter = formatter;
public JavaTimeReaderWriterProvider withLocalTimeZone(ZoneId localZoneId) {
_localZoneId = localZoneId == null ? ZoneId.of("Z") : localZoneId;
return this;
}

/**
* Convenience method to quickly set the system default time zone as the preferred one. This
* is equivalent to calling: <code>withLocalTimeZone(ZoneId.systemDefault())</code>.
* @since 2.20
* @return Reference for chaining
*/
public JavaTimeReaderWriterProvider withSystemDefaultTimeZone() {
return withLocalTimeZone(ZoneId.systemDefault());
}

/**
* 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 that do not have an offset. Set to
* <code>false</code> when handling local date times.
* @since 2.20
* @return Formatter
*/
private 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) {
// Without this, parsing a ZonedDateTime or OffsetDateTime will cause an exception if
// no offset was specified. So we default the offset to UTC to be safe.
builder.parseDefaulting(ChronoField.OFFSET_SECONDS, 0);
}

return builder.toFormatter();
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,59 @@
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.JSONObjectException;
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);
if (p.hasToken(JsonToken.VALUE_NULL)) {
return null;
}
if (p.hasToken(JsonToken.VALUE_STRING)) {
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
// local date time
return ((ZonedDateTime)ta).withZoneSameInstant(_localZoneId).toLocalDateTime();
}
if (ta instanceof LocalDateTime) {
return ta;
}
}

throw JSONObjectException.from(p,
"Can not create a "+_valueType.getName()+" instance out of "+_tokenDesc(p));
}
}
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,43 @@
package com.fasterxml.jackson.jr.extension.javatime;

import java.io.IOException;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Objects;

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

/**
* {@link ValueReader} that converts an ISO 8601 string to a {@link ZonedDateTime} instance.
* @see <a href="https://en.wikipedia.org/wiki/ISO_8601">ISO 8601 on Wikipedia</a>
* @since 2.20
*/
public class OffsetDateTimeValueReader extends DefaultDateTimeValueReader<OffsetDateTime> {
private final ZoneId _localZoneId;

/**
* Constructor that accepts a zone ID as a parameter. The zone ID is configured when no zone
* or offset was configured in the ISO 8601 string.
* @param localZoneId Local zone ID
*/
public OffsetDateTimeValueReader(ZoneId localZoneId) {
super(OffsetDateTime.class, OffsetDateTime::from);
_localZoneId = Objects.requireNonNull(localZoneId);
}

@Override
public Object read(JSONReader reader, JsonParser p) throws IOException {
final OffsetDateTime odt = (OffsetDateTime)super.read(reader, p);

// If the offset was missing, our formatter defaults to UTC. However, we need to set it
// to our preferred local zone to get ISO 8601 compliant behavior. In case our preferred
// zone already is UTC, no harm is done here.
if (odt != null && offsetMissing(p.getText())) {
return odt.withOffsetSameLocal(_localZoneId.getRules().getOffset(odt.toInstant()));
}
return odt;
}
}
Loading