From a0e4ac39bf9430ec995ce4bb321ee694d742ffb9 Mon Sep 17 00:00:00 2001 From: monosoul Date: Wed, 30 Oct 2019 15:14:13 +0100 Subject: [PATCH] Inverse condition to fix ISO-formatted Instant parsing Prior to this commit, InstantFormatter was able to properly serialize an Instant that is far in the future (or in the past), but it could not properly deserialize it, because in such scenarios an ISO-formatted Instant starts with a +/- sign. This commit fixes this issue, while maintaining the previous contract, and also introduces tests for InstantFormatter. Closes gh-23895 --- .../datetime/standard/InstantFormatter.java | 10 +- .../standard/InstantFormatterTests.java | 119 ++++++++++++++++++ 2 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java index 985006e3ba3..010d004ae5d 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java @@ -40,14 +40,14 @@ public class InstantFormatter implements Formatter { @Override public Instant parse(String text, Locale locale) throws ParseException { - if (text.length() > 0 && Character.isDigit(text.charAt(0))) { - // assuming UTC instant a la "2007-12-03T10:15:30.00Z" - return Instant.parse(text); - } - else { + if (text.length() > 0 && Character.isAlphabetic(text.charAt(0))) { // assuming RFC-1123 value a la "Tue, 3 Jun 2008 11:05:30 GMT" return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(text)); } + else { + // assuming UTC instant a la "2007-12-03T10:15:30.00Z" + return Instant.parse(text); + } } @Override diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java new file mode 100644 index 00000000000..5e9b0814fcb --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.format.datetime.standard; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import java.text.ParseException; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Random; +import java.util.stream.Stream; + +import static java.time.Instant.MAX; +import static java.time.Instant.MIN; +import static java.time.ZoneId.systemDefault; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Andrei Nevedomskii + */ +@SuppressWarnings("ConstantConditions") +class InstantFormatterTests { + + private final InstantFormatter instantFormatter = new InstantFormatter(); + + @ParameterizedTest + @ArgumentsSource(ISOSerializedInstantProvider.class) + void should_parse_an_ISO_formatted_string_representation_of_an_instant(String input) throws ParseException { + Instant expected = DateTimeFormatter.ISO_INSTANT.parse(input, Instant::from); + + Instant actual = instantFormatter.parse(input, null); + + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @ArgumentsSource(RFC1123SerializedInstantProvider.class) + void should_parse_an_RFC1123_formatted_string_representation_of_an_instant(String input) throws ParseException { + Instant expected = DateTimeFormatter.RFC_1123_DATE_TIME.parse(input, Instant::from); + + Instant actual = instantFormatter.parse(input, null); + + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @ArgumentsSource(RandomInstantProvider.class) + void should_serialize_an_instant_using_ISO_format_and_ignoring_locale(Instant input) { + String expected = DateTimeFormatter.ISO_INSTANT.format(input); + + String actual = instantFormatter.print(input, null); + + assertThat(actual).isEqualTo(expected); + } + + private static class ISOSerializedInstantProvider extends RandomInstantProvider { + + @Override + Stream provideArguments() { + return randomInstantStream(MIN, MAX).map(DateTimeFormatter.ISO_INSTANT::format); + } + } + + private static class RFC1123SerializedInstantProvider extends RandomInstantProvider { + + // RFC-1123 supports only 4-digit years + private final Instant min = Instant.parse("0000-01-01T00:00:00.00Z"); + + private final Instant max = Instant.parse("9999-12-31T23:59:59.99Z"); + + @Override + Stream provideArguments() { + return randomInstantStream(min, max) + .map(DateTimeFormatter.RFC_1123_DATE_TIME.withZone(systemDefault())::format); + } + } + + private static class RandomInstantProvider implements ArgumentsProvider { + + private static final long DATA_SET_SIZE = 10; + + static final Random RANDOM = new Random(); + + Stream provideArguments() { + return randomInstantStream(MIN, MAX); + } + + @Override + public final Stream provideArguments(ExtensionContext context) { + return provideArguments().map(Arguments::of).limit(DATA_SET_SIZE); + } + + Stream randomInstantStream(Instant min, Instant max) { + return Stream.concat( + Stream.of(Instant.now()), // make sure that the data set includes current instant + RANDOM.longs(min.getEpochSecond(), max.getEpochSecond()) + .mapToObj(Instant::ofEpochSecond) + ); + } + } +} \ No newline at end of file