Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a regex checker and fixer for offsets without colons for ZDateTime and OSDateTime #208

Merged
merged 4 commits into from
Apr 1, 2021
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
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