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;
    }
}