IntervalDeserializer.java
package tools.jackson.datatype.joda.deser;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Interval;
import org.joda.time.Period;
import org.joda.time.PeriodType;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISOPeriodFormat;
import org.joda.time.format.PeriodFormatter;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.datatype.joda.cfg.FormatConfig;
import tools.jackson.datatype.joda.cfg.JacksonJodaDateFormat;
public class IntervalDeserializer extends JodaDateDeserializerBase<Interval>
{
// Same parsers as used by `Interval.parseWithOffset()`
private static final PeriodFormatter ISO_PERIOD_PARSER = ISOPeriodFormat.standard()
.withParseType(PeriodType.standard());
public IntervalDeserializer() {
// NOTE: not currently used, but must pass something
this(FormatConfig.DEFAULT_DATETIME_PARSER);
}
public IntervalDeserializer(JacksonJodaDateFormat format) {
super(Interval.class, format);
}
@Override
public JodaDateDeserializerBase<?> withFormat(JacksonJodaDateFormat format) {
return new IntervalDeserializer(format);
}
@Override
public Interval deserialize(JsonParser p, DeserializationContext ctxt)
throws JacksonException
{
if (p.hasToken(JsonToken.VALUE_STRING)) {
return _fromString(p, ctxt, p.getString());
}
// 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML)
if (p.isExpectedStartObjectToken()) {
// 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 (Interval) ctxt.handleUnexpectedToken(getValueType(ctxt),
p.currentToken(), p, "expected JSON String");
}
// @since 2.12
protected Interval _fromString(final JsonParser p, final DeserializationContext ctxt,
String value)
throws JacksonException
{
value = value.trim();
if (value.isEmpty()) {
return _fromEmptyString(p, ctxt, value);
}
// 17-Nov-2014, tatu: Actually let's start with slash, instead of hyphen, because
// that is the separator for standard functionality...
int index = value.indexOf('/', 1);
boolean hasSlash = (index > 0);
if (!hasSlash) {
index = value.indexOf('-', 1);
}
if (index < 0) {
// [datatype-joda#200]: report via handler (like other failures), not throw directly
return (Interval) ctxt.handleWeirdStringValue(handledType(), value,
"no slash or hyphen found to separate start, end");
}
long start, end;
String str = value.substring(0, index);
Interval result;
try {
// !!! TODO: configurable formats...
if (hasSlash) {
result = _fromISOString(ctxt, value, index);
} else {
start = Long.parseLong(str);
str = value.substring(index + 1);
end = Long.parseLong(str);
// 20-Oct-2026: instants are zone-independent, but use same zone as for
// ISO form (not JVM default one) for consistent chronology of result
result = new Interval(start, end, _intervalZone(ctxt));
}
} catch (NumberFormatException e) {
// [datatype-joda#200]: report full value (like other failures)
return (Interval) _handleWeirdStringValue(ctxt, e, value,
"Failed to parse number from '%s'", str);
} catch (IllegalArgumentException e) {
// [datatype-joda#195]: do not let raw Joda exception escape
// (invalid start/end, or end before start)
return (Interval) _handleWeirdStringValue(ctxt, e, value,
"%s", e.getMessage());
} catch (ArithmeticException e) {
// [datatype-joda#200]: overflow applying period/duration or offset
return (Interval) _handleWeirdStringValue(ctxt, e, value,
"value out of range (overflow): %s", e.getMessage());
}
final DateTimeZone contextTimezone =
_format.shouldAdjustToContextTimeZone(ctxt) ? _contextTimeZone(ctxt) : null;
DateTimeZone tz = _format.isTimezoneExplicit() ? _format.getTimeZone() : contextTimezone;
if (tz != null) {
if (!tz.equals(result.getStart().getZone())) {
result = new Interval(result.getStartMillis(), result.getEndMillis(), tz);
}
}
return result;
}
/**
* Helper method for parsing ISO-8601 interval notation ({@code start/end},
* {@code start/period} or {@code period/end}), same as
* {@link Interval#parseWithOffset} except that date/time values without
* offset are resolved using the configured time zone (explicit format time
* zone if any, context time zone otherwise) and not the JVM default one.
*
* @since 3.3
*/
protected Interval _fromISOString(DeserializationContext ctxt, String value, int separator)
{
final String leftStr = value.substring(0, separator);
final String rightStr = value.substring(separator + 1);
final DateTimeFormatter parser = JacksonJodaDateFormat.withZoneViaChronology(
JacksonJodaDateFormat.ISO_DATETIME_OFFSET_PARSER, _intervalZone(ctxt));
DateTime start = null;
Period period = null;
if (_isPeriod(leftStr)) {
period = ISO_PERIOD_PARSER.parsePeriod(leftStr);
} else {
start = JacksonJodaDateFormat.parseDateTime(parser, leftStr);
}
if (_isPeriod(rightStr)) {
if (period != null) {
throw new IllegalArgumentException("Interval composed of two durations: " + value);
}
return new Interval(start, ISO_PERIOD_PARSER.parsePeriod(rightStr));
}
final DateTime end = JacksonJodaDateFormat.parseDateTime(parser, rightStr);
if (period != null) {
return new Interval(period, end);
}
return new Interval(start, end);
}
/**
* Time zone used for values without offset and for chronology of results
* of both String forms: explicit format time zone if any, context time zone
* otherwise (regardless of whether adjustment to context zone is enabled).
*/
private DateTimeZone _intervalZone(DeserializationContext ctxt) {
return _format.isTimezoneExplicit() ? _format.getTimeZone() : _contextTimeZone(ctxt);
}
private static boolean _isPeriod(String str) {
if (str.isEmpty()) {
return false;
}
final char c = str.charAt(0);
return (c == 'P') || (c == 'p');
}
}