Skip to content

Commit aa76baf

Browse files
authored
Merge pull request #19 from kevinjom/master
Supported parsing date time text with valid zero zone offsets
2 parents 2951232 + 898bd4f commit aa76baf

File tree

3 files changed

+85
-25
lines changed

3 files changed

+85
-25
lines changed

datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java

+30-18
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import java.time.temporal.TemporalAccessor;
4040
import java.util.function.BiFunction;
4141
import java.util.function.Function;
42+
import java.util.regex.Pattern;
4243

4344
/**
4445
* Deserializer for Java 8 temporal {@link Instant}s, {@link OffsetDateTime}, and {@link ZonedDateTime}s.
@@ -51,13 +52,20 @@ public class InstantDeserializer<T extends Temporal>
5152
{
5253
private static final long serialVersionUID = 1L;
5354

55+
/**
56+
* Constants used to check if the time offset is zero. See [jackson-modules-java8#18]
57+
*
58+
* @since 2.9.0
59+
*/
60+
private static final Pattern ISO8601_UTC_ZERO_OFFSET_SUFFIX_REGEX = Pattern.compile("\\+00:?(00)?$");
61+
5462
public static final InstantDeserializer<Instant> INSTANT = new InstantDeserializer<>(
5563
Instant.class, DateTimeFormatter.ISO_INSTANT,
5664
Instant::from,
5765
a -> Instant.ofEpochMilli(a.value),
5866
a -> Instant.ofEpochSecond(a.integer, a.fraction),
5967
null,
60-
true // yes, replace +0000 with Z
68+
true // yes, replace zero offset with Z
6169
);
6270

6371
public static final InstantDeserializer<OffsetDateTime> OFFSET_DATE_TIME = new InstantDeserializer<>(
@@ -66,7 +74,7 @@ public class InstantDeserializer<T extends Temporal>
6674
a -> OffsetDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId),
6775
a -> OffsetDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId),
6876
(d, z) -> d.withOffsetSameInstant(z.getRules().getOffset(d.toLocalDateTime())),
69-
true // yes, replace +0000 with Z
77+
true // yes, replace zero offset with Z
7078
);
7179

7280
public static final InstantDeserializer<ZonedDateTime> ZONED_DATE_TIME = new InstantDeserializer<>(
@@ -75,7 +83,7 @@ public class InstantDeserializer<T extends Temporal>
7583
a -> ZonedDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId),
7684
a -> ZonedDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId),
7785
ZonedDateTime::withZoneSameInstant,
78-
false // keep +0000 and Z separate since zones explicitly supported
86+
false // keep zero offset and Z separate since zones explicitly supported
7987
);
8088

8189
protected final Function<FromIntegerArguments, T> fromMilliseconds;
@@ -87,13 +95,13 @@ public class InstantDeserializer<T extends Temporal>
8795
protected final BiFunction<T, ZoneId, T> adjust;
8896

8997
/**
90-
* In case of vanilla `Instant` we seem to need to translate "+0000"
98+
* In case of vanilla `Instant` we seem to need to translate "+0000 | +00:00 | +00"
9199
* timezone designator into plain "Z" for some reason; see
92-
* [datatype-jsr310#79] for more info
100+
* [jackson-modules-java8#18] for more info
93101
*
94-
* @since 2.7.5
102+
* @since 2.9.0
95103
*/
96-
protected final boolean replace0000AsZ;
104+
protected final boolean replaceZeroOffsetAsZ;
97105

98106
/**
99107
* Flag for <code>JsonFormat.Feature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE</code>
@@ -108,14 +116,14 @@ protected InstantDeserializer(Class<T> supportedType,
108116
Function<FromIntegerArguments, T> fromMilliseconds,
109117
Function<FromDecimalArguments, T> fromNanoseconds,
110118
BiFunction<T, ZoneId, T> adjust,
111-
boolean replace0000AsZ)
119+
boolean replaceZeroOffsetAsZ)
112120
{
113121
super(supportedType, formatter);
114122
this.parsedToValue = parsedToValue;
115123
this.fromMilliseconds = fromMilliseconds;
116124
this.fromNanoseconds = fromNanoseconds;
117125
this.adjust = adjust == null ? ((d, z) -> d) : adjust;
118-
this.replace0000AsZ = replace0000AsZ;
126+
this.replaceZeroOffsetAsZ = replaceZeroOffsetAsZ;
119127
_adjustToContextTZOverride = null;
120128
}
121129

@@ -127,7 +135,7 @@ protected InstantDeserializer(InstantDeserializer<T> base, DateTimeFormatter f)
127135
fromMilliseconds = base.fromMilliseconds;
128136
fromNanoseconds = base.fromNanoseconds;
129137
adjust = base.adjust;
130-
replace0000AsZ = (_formatter == DateTimeFormatter.ISO_INSTANT);
138+
replaceZeroOffsetAsZ = (_formatter == DateTimeFormatter.ISO_INSTANT);
131139
_adjustToContextTZOverride = base._adjustToContextTZOverride;
132140
}
133141

@@ -139,7 +147,7 @@ protected InstantDeserializer(InstantDeserializer<T> base, Boolean adjustToConte
139147
fromMilliseconds = base.fromMilliseconds;
140148
fromNanoseconds = base.fromNanoseconds;
141149
adjust = base.adjust;
142-
replace0000AsZ = base.replace0000AsZ;
150+
replaceZeroOffsetAsZ = base.replaceZeroOffsetAsZ;
143151
_adjustToContextTZOverride = adjustToContextTimezoneOverride;
144152
}
145153

@@ -189,13 +197,8 @@ public T deserialize(JsonParser parser, DeserializationContext context) throws I
189197
// fall through to default handling, to get error there
190198
}
191199
}
192-
// 24-May-2016, tatu: as per [datatype-jsr310#79] seems like we need
193-
// some massaging in some cases...
194-
if (replace0000AsZ) {
195-
if (string.endsWith("+0000")) {
196-
string = string.substring(0, string.length() - 5) + "Z";
197-
}
198-
}
200+
201+
string = replaceZeroOffsetAsZIfNecessary(string);
199202
}
200203

201204
T value;
@@ -285,6 +288,15 @@ private ZoneId getZone(DeserializationContext context)
285288
return (_valueClass == Instant.class) ? null : context.getTimeZone().toZoneId();
286289
}
287290

291+
private String replaceZeroOffsetAsZIfNecessary(String text)
292+
{
293+
if (replaceZeroOffsetAsZ) {
294+
return ISO8601_UTC_ZERO_OFFSET_SUFFIX_REGEX.matcher(text).replaceFirst("Z");
295+
}
296+
297+
return text;
298+
}
299+
288300
public static class FromIntegerArguments // since 2.8.3
289301
{
290302
public final long value;

datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/TestInstantSerialization.java

+37-7
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616

1717
package com.fasterxml.jackson.datatype.jsr310;
1818

19-
import static org.junit.Assert.assertEquals;
20-
import static org.junit.Assert.assertNotNull;
21-
import static org.junit.Assert.assertTrue;
19+
import com.fasterxml.jackson.annotation.JsonFormat;
20+
import com.fasterxml.jackson.databind.DeserializationFeature;
21+
import com.fasterxml.jackson.databind.ObjectMapper;
22+
import com.fasterxml.jackson.databind.SerializationFeature;
23+
import org.junit.Test;
2224

2325
import java.time.Instant;
2426
import java.time.LocalDate;
@@ -29,10 +31,9 @@
2931
import java.time.temporal.ChronoUnit;
3032
import java.time.temporal.Temporal;
3133

32-
import com.fasterxml.jackson.annotation.JsonFormat;
33-
import com.fasterxml.jackson.databind.*;
34-
35-
import org.junit.Test;
34+
import static org.junit.Assert.assertEquals;
35+
import static org.junit.Assert.assertNotNull;
36+
import static org.junit.Assert.assertTrue;
3637

3738
public class TestInstantSerialization extends ModuleTestBase
3839
{
@@ -443,4 +444,33 @@ public void testRoundTripOfInstantAndJavaUtilDate() throws Exception
443444

444445
assertEquals(givenInstant, actual);
445446
}
447+
448+
// [jackson-modules-java8#18]
449+
@Test
450+
public void testDeserializationFromStringWithZeroZoneOffset01() throws Exception {
451+
Instant date = Instant.now();
452+
String json = formatWithZeroZoneOffset(date, "+00:00");
453+
Instant result = MAPPER.readValue(json, Instant.class);
454+
assertEquals("The value is not correct.", date, result);
455+
}
456+
457+
@Test
458+
public void testDeserializationFromStringWithZeroZoneOffset02() throws Exception {
459+
Instant date = Instant.now();
460+
String json = formatWithZeroZoneOffset(date, "+0000");
461+
Instant result = MAPPER.readValue(json, Instant.class);
462+
assertEquals("The value is not correct.", date, result);
463+
}
464+
465+
@Test
466+
public void testDeserializationFromStringWithZeroZoneOffset03() throws Exception {
467+
Instant date = Instant.now();
468+
String json = formatWithZeroZoneOffset(date, "+00");
469+
Instant result = MAPPER.readValue(json, Instant.class);
470+
assertEquals("The value is not correct.", date, result);
471+
}
472+
473+
private String formatWithZeroZoneOffset(Instant date, String offset){
474+
return '"' + FORMATTER.format(date).replaceFirst("Z$", offset) + '"';
475+
}
446476
}

datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/TestOffsetDateTimeDeserialization.java

+18
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,24 @@ public void testBadDeserializationAsString01() throws Throwable
8484
expectFailure("'notanoffsetdatetime'");
8585
}
8686

87+
@Test
88+
public void testDeserializationAsWithZeroZoneOffset01() throws Exception
89+
{
90+
expectSuccess(OffsetDateTime.of(2000, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC), "'2000-01-01T12:00+00:00'");
91+
}
92+
93+
@Test
94+
public void testDeserializationAsWithZeroZoneOffset02() throws Exception
95+
{
96+
expectSuccess(OffsetDateTime.of(2000, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC), "'2000-01-01T12:00+0000'");
97+
}
98+
99+
@Test
100+
public void testDeserializationAsWithZeroZoneOffset03() throws Exception
101+
{
102+
expectSuccess(OffsetDateTime.of(2000, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC), "'2000-01-01T12:00+00'");
103+
}
104+
87105
private void expectFailure(String json) throws Exception {
88106
try {
89107
read(json);

0 commit comments

Comments
 (0)