Skip to content

Commit

Permalink
Added a regex checker and fixer for offsets without colons for ZDateT…
Browse files Browse the repository at this point in the history
…ime and OSDateTime (#208)

* Added a regex checker and fixer for offsets without colons for ZonedDateTime and OffsetDateTime.
* Added check at start of regex to avoid parsing plus and negative years ...
* Added additional tests for ISO8601_COLONLESS_OFFSET_REGEX expression and recommented on regex expression usage in production.
* Altered the Regex format for checking datetime offsets. Previously the string checked the whole string beyond the start, now it checks at the end or before [.

Co-authored-by: oh <[email protected]>
  • Loading branch information
oeystein and oh authored Apr 1, 2021
1 parent 8b30c9b commit 31a46ad
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
Expand All @@ -59,6 +60,13 @@ public class InstantDeserializer<T extends Temporal>
*/
private static final Pattern ISO8601_UTC_ZERO_OFFSET_SUFFIX_REGEX = Pattern.compile("\\+00:?(00)?$");

/**
* Constants used to check if ISO 8601 time string is colonless. See [jackson-modules-java8#131]
*
* @since 2.13
*/
protected static final Pattern ISO8601_COLONLESS_OFFSET_REGEX = Pattern.compile("[+-][0-9]{4}(?=\\[|$)");

public static final InstantDeserializer<Instant> INSTANT = new InstantDeserializer<>(
Instant.class, DateTimeFormatter.ISO_INSTANT,
Instant::from,
Expand Down Expand Up @@ -277,6 +285,17 @@ protected T _fromString(JsonParser p, DeserializationContext ctxt,
string = replaceZeroOffsetAsZIfNecessary(string);
}

// For some reason DateTimeFormatter.ISO_INSTANT only supports UTC ISO 8601 strings, so it have to be excluded
if (_formatter == DateTimeFormatter.ISO_OFFSET_DATE_TIME ||
_formatter == DateTimeFormatter.ISO_ZONED_DATE_TIME) {

// 21-March-2021, Oeystein: Work-around to support basic iso 8601 format (colon-less).
// As per JSR-310; Only extended 8601 formats (with colon) are supported for
// ZonedDateTime.parse() and OffsetDateTime.parse().
// https://github.com/FasterXML/jackson-modules-java8/issues/131
string = addInColonToOffsetIfMissing(string);
}

T value;
try {
TemporalAccessor acc = _formatter.parse(string);
Expand Down Expand Up @@ -323,6 +342,20 @@ private String replaceZeroOffsetAsZIfNecessary(String text)
return text;
}

private String addInColonToOffsetIfMissing(String text)
{
Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher(text);

if (matcher.find()){
StringBuilder sb = new StringBuilder(matcher.group(0));
sb.insert(3, ":");

return matcher.replaceFirst(sb.toString());
}

return text;
}

public static class FromIntegerArguments // since 2.8.3
{
public final long value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.util.Map;
import java.util.regex.Matcher;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
Expand All @@ -19,6 +20,7 @@
import com.fasterxml.jackson.datatype.jsr310.MockObjectConfiguration;
import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase;

import static com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer.ISO8601_COLONLESS_OFFSET_REGEX;
import static org.junit.Assert.*;
import static org.junit.Assert.assertNull;

Expand Down Expand Up @@ -522,4 +524,40 @@ public void testStrictDeserializeFromEmptyString() throws Exception {
String valueFromEmptyStr = mapper.writeValueAsString(asMap(key, ""));
objectReader.readValue(valueFromEmptyStr);
}

/*
/************************************************************************
/* Tests for InstantDeserializer.ISO8601_COLONLESS_OFFSET_REGEX
/************************************************************************
*/
@Test
public void testISO8601ColonlessRegexFindsOffset() {
Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher("2000-01-01T12:00+0100");

assertTrue("Matcher finds +0100 as an colonless offset", matcher.find());
assertEquals("Matcher groups +0100 as an colonless offset", matcher.group(), "+0100");
}

@Test
public void testISO8601ColonlessRegexFindsOffsetWithTZ() {
Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher("2000-01-01T12:00+0100[Europe/Paris]");

assertTrue("Matcher finds +0100 as an colonless offset", matcher.find());
assertEquals("Matcher groups +0100 as an colonless offset", matcher.group(), "+0100");
}

@Test
public void testISO8601ColonlessRegexDoesNotAffectNegativeYears() {
Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher("-2000-01-01T12:00+01:00[Europe/Paris]");

assertFalse("Matcher does not find -2000 (years) as an offset without colon", matcher.find());
}

@Test
public void testISO8601ColonlessRegexDoesNotAffectNegativeYearsWithColonless() {
Matcher matcher = ISO8601_COLONLESS_OFFSET_REGEX.matcher("-2000-01-01T12:00+0100[Europe/Paris]");

assertTrue("Matcher finds +0100 as an colonless offset", matcher.find());
assertEquals("Matcher groups +0100 as an colonless offset", matcher.group(), "+0100");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,22 @@ public void testDeserializationAsString01WithTimeZoneTurnedOff() throws Exceptio
assertEquals("The time zone is not correct.", getOffset(value, Z1), value.getOffset());
}

@Test
public void testDeserializationAsString01WithTimeZoneColonless() throws Exception
{
OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(0L), Z1);
ObjectMapper m = newMapper()
.configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false);

String sDate = offsetWithoutColon(FORMATTER.format(date));

OffsetDateTime value = m.readValue('"' + sDate + '"', OffsetDateTime.class);

assertNotNull("The value should not be null.", value);
assertIsEqual(date, value);
assertEquals("The time zone is not correct.", getOffset(value, Z1), value.getOffset());
}

@Test
public void testDeserializationAsString02WithoutTimeZone() throws Exception
{
Expand Down Expand Up @@ -384,6 +400,22 @@ public void testDeserializationAsString02WithTimeZoneTurnedOff() throws Exceptio
assertEquals("The time zone is not correct.", getOffset(value, Z2), value.getOffset());
}

@Test
public void testDeserializationAsString02WithTimeZoneColonless() throws Exception
{
OffsetDateTime date = OffsetDateTime.ofInstant(Instant.ofEpochSecond(123456789L, 183917322), Z2);
ObjectMapper m = newMapper()
.configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false);

String sDate = offsetWithoutColon(FORMATTER.format(date));

OffsetDateTime value = m.readValue('"' + sDate + '"', OffsetDateTime.class);

assertNotNull("The value should not be null.", value);
assertIsEqual(date, value);
assertEquals("The time zone is not correct.", getOffset(value, Z2), value.getOffset());
}

@Test
public void testDeserializationAsString03WithoutTimeZone() throws Exception
{
Expand Down Expand Up @@ -425,6 +457,23 @@ public void testDeserializationAsString03WithTimeZoneTurnedOff() throws Exceptio
assertEquals("The time zone is not correct.", getOffset(value, Z3), value.getOffset());
}


@Test
public void testDeserializationAsString03WithTimeZoneColonless() throws Exception
{
OffsetDateTime date = OffsetDateTime.now(Z3);
ObjectMapper m = newMapper()
.configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false);

String sDate = offsetWithoutColon(FORMATTER.format(date));

OffsetDateTime value = m.readValue('"' + sDate + '"', OffsetDateTime.class);

assertNotNull("The value should not be null.", value);
assertIsEqual(date, value);
assertEquals("The time zone is not correct.", getOffset(value, Z3), value.getOffset());
}

@Test
public void testDeserializationWithTypeInfo01WithoutTimeZone() throws Exception
{
Expand Down Expand Up @@ -709,4 +758,8 @@ private static ZoneOffset getOffset(OffsetDateTime date, ZoneId zone)
{
return zone.getRules().getOffset(date.toLocalDateTime());
}

private static String offsetWithoutColon(String string){
return new StringBuilder(string).deleteCharAt(string.lastIndexOf(":")).toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,36 @@ public void testStrictDeserializeFromEmptyString() throws Exception {
objectReader.readValue(valueFromEmptyStr);
}

/*
/**********************************************************
/ Tests for Iso 8601s ZonedDateTimes that are colonless
/**********************************************************
*/

@Test
public void testDeserializationWithoutColonInOffset() throws Throwable
{
WrapperWithFeatures wrapper = newMapper()
.readerFor(WrapperWithFeatures.class)
.readValue("{\"value\":\"2000-01-01T12:00+0100\"}");

assertEquals("Value parses as if it were with colon",
ZonedDateTime.of(2000, 1, 1, 12, 0, 0 ,0, ZoneOffset.ofHours(1)),
wrapper.value);
}

@Test
public void testDeserializationWithoutColonInTimeZoneWithTZDB() throws Throwable
{
WrapperWithFeatures wrapper = newMapper()
.readerFor(WrapperWithFeatures.class)
.readValue("{\"value\":\"2000-01-01T12:00+0100[Europe/Paris]\"}");
assertEquals("Timezone should be preserved.",
ZonedDateTime.of(2000, 1, 1, 12, 0, 0 ,0, ZoneId.of("Europe/Paris")),
wrapper.value);
}


private void expectFailure(String json) throws Throwable {
try {
READER.readValue(a2q(json));
Expand Down

0 comments on commit 31a46ad

Please sign in to comment.