DateTimeDeserializer.java
package tools.jackson.datatype.joda.deser;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.ReadableDateTime;
import org.joda.time.ReadableInstant;
import org.joda.time.format.DateTimeFormatter;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonTokenId;
import tools.jackson.core.StreamReadCapability;
import tools.jackson.core.io.NumberInput;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.ValueDeserializer;
import tools.jackson.datatype.joda.cfg.FormatConfig;
import tools.jackson.datatype.joda.cfg.JacksonJodaDateFormat;
/**
* Basic deserializer for {@link ReadableDateTime} and its subtypes.
* Accepts JSON String and Number values and passes those to single-argument constructor.
* Does not (yet?) support JSON object; support can be added if desired.
*/
public class DateTimeDeserializer
extends JodaDateDeserializerBase<ReadableInstant>
{
public DateTimeDeserializer(Class<?> cls, JacksonJodaDateFormat format) {
super(cls, format);
}
/**
* @since 2.14 (to allow use via annotations)
*/
public DateTimeDeserializer() {
super(DateTime.class, FormatConfig.DEFAULT_DATETIME_PARSER);
}
@SuppressWarnings("unchecked")
public static <T extends ReadableInstant> ValueDeserializer<T> forType(Class<T> cls)
{
return (ValueDeserializer<T>) new DateTimeDeserializer(cls,
FormatConfig.DEFAULT_DATETIME_PARSER);
}
@Override
public JodaDateDeserializerBase<?> withFormat(JacksonJodaDateFormat format) {
return new DateTimeDeserializer(_valueClass, format);
}
@Override
public ReadableInstant deserialize(JsonParser p, DeserializationContext ctxt)
throws JacksonException
{
switch (p.currentTokenId()) {
case JsonTokenId.ID_NUMBER_INT:
return _fromTimestamp(ctxt, p.getLongValue(), null);
case JsonTokenId.ID_STRING:
return _fromString(p, ctxt, p.getString());
case JsonTokenId.ID_START_OBJECT:
// 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML)
String str = ctxt.extractScalarFromObject(p, this, handledType());
// 17-May-2025, tatu: [databind#4656] need to check for `null`
if (str != null) {
return _fromString(p, ctxt, str);
}
// fall through
}
return _handleNotNumberOrString(p, ctxt);
}
// @since 2.12
protected ReadableInstant _fromString(final JsonParser p, final DeserializationContext ctxt,
String value)
throws JacksonException
{
value = value.trim();
if (value.isEmpty()) {
return _fromEmptyString(p, ctxt, value);
}
// 08-Jul-2015, tatu: as per [datatype-joda#44], optional TimeZone inclusion
// NOTE: on/off feature only for serialization; on deser should accept both
// 07-Oct-2026: [datatype-joda#191] zone id suffix has to close the value;
// and is the LAST '[...]' (custom patterns may contain literal brackets)
final int ix = value.endsWith("]") ? value.lastIndexOf('[') : -1;
String tzId = null;
DateTimeZone tz = null;
if (ix > 0) {
tzId = value.substring(ix+1, value.length() - 1);
try {
tz = DateTimeZone.forID(tzId);
} catch (IllegalArgumentException e) {
// Not a zone id: may be literal brackets at end of custom pattern
// (handled below, along with other literal brackets)
}
}
if (tz != null) {
final String fullValue = value;
value = value.substring(0, ix);
// 12-Jul-2015, tatu: Initially planned to support "timestamp[zone-id]"
// format as well as textual, but since JSR-310 datatype (Java 8 datetime)
// does not support it, was left out of 2.6.
/*
// One more thing; do we have plain timestamp?
if (_allDigits(str)) {
return new DateTime(Long.parseLong(str), tz);
}
*/
DateTime result;
try {
result = JacksonJodaDateFormat.parseDateTime(
_format.createParser(ctxt).withZone(tz), value);
} catch (IllegalArgumentException | ArithmeticException e) {
// Bracketed value may instead be literal part of custom pattern
// (like offset in "yyyy-MM-dd'['ZZ']'"), so try full value first
try {
return JacksonJodaDateFormat.parseDateTime(_format.createParser(ctxt), fullValue);
} catch (IllegalArgumentException | ArithmeticException e2) {
// not valid that way either: report original failure
}
// 07-Oct-2026: [datatype-joda#191] do not let raw Joda exception escape
return (ReadableInstant) _handleWeirdStringValue(ctxt, e, fullValue,
"Invalid date/time before DateTimeZone id suffix: %s", e.getMessage());
}
// 23-Jul-2017, tatu: As per [datatype-joda#93] only override tz if allowed to
if (_format.shouldAdjustToContextTimeZone(ctxt)) {
// [datatype-joda#194]: use context time zone unless explicitly configured
result = result.withZone(_format.isTimezoneExplicit()
? _format.getTimeZone() : _contextTimeZone(ctxt));
}
return result;
}
// 14-Jul-2020: [datatype-joda#117] Should allow use of "Timestamp as String" for
// some textual formats
if (ctxt.isEnabled(StreamReadCapability.UNTYPED_SCALARS)
&& _isValidTimestampString(value)) {
return _fromTimestamp(ctxt, NumberInput.parseLong(value), value);
}
// Not sure if it should use timezone or not...
// 15-Sep-2015, tatu: impl of 'createParser()' SHOULD handle all timezone/locale setup
final int openIx = value.indexOf('[');
if (openIx > 0) {
// 07-Oct-2026: [datatype-joda#191] Not a valid zone id suffix, but may still
// be valid for a custom pattern with literal '['
try {
return JacksonJodaDateFormat.parseDateTime(_format.createParser(ctxt), value);
} catch (IllegalArgumentException e) {
final String msg;
if (tzId != null) {
msg = "Unknown DateTimeZone id '"+tzId+"'";
} else if (value.indexOf(']', openIx) < 0) {
msg = "Malformed DateTimeZone id suffix: missing closing ']'";
} else {
msg = "Malformed DateTimeZone id suffix: unexpected content after closing ']'";
}
// [datatype-joda#200]: include Joda's message (may be invalid date/time value)
return (ReadableInstant) _handleWeirdStringValue(ctxt, e, value,
"%s (%s)", msg, e.getMessage());
} catch (ArithmeticException e) {
// [datatype-joda#200]: overflow, not a malformed suffix (but may
// still need to report unknown zone id)
if (tzId != null) {
return (ReadableInstant) _handleWeirdStringValue(ctxt, e, value,
"Unknown DateTimeZone id '%s' (%s)", tzId, e.getMessage());
}
return (ReadableInstant) _handleWeirdStringValue(ctxt, e, value,
"%s", e.getMessage());
}
}
final DateTimeFormatter parser = _format.createParser(ctxt);
return _parseWithJoda(ctxt, value, v -> JacksonJodaDateFormat.parseDateTime(parser, v));
}
protected DateTime _fromTimestamp(DeserializationContext ctxt, long ts) {
return (DateTime) _fromTimestamp(ctxt, ts, null);
}
// @since 3.3
protected ReadableInstant _fromTimestamp(DeserializationContext ctxt, long ts, String text) {
// [datatype-joda#199]: zone offset addition for edge-of-long timestamp can
// overflow; do not let raw Joda exception escape
return _fromTimestamp(ctxt, ts, text, (t, zone) -> {
DateTime dt = new DateTime(t, zone);
// Joda silently drops non-UTC zone for instants at the very edge of
// supported range: must not return value with wrong zone
if (!zone.equals(dt.getZone())) {
throw new IllegalArgumentException(String.format(
"Timestamp out of supported range for time zone '%s'", zone.getID()));
}
return dt;
});
}
// Edge-of-long timestamps are valid in UTC (unlike local date/time ones),
// and verified by checking resulting zone instead
@Override
protected boolean _verifyTimestampYear() {
return false;
}
}