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 985006e3ba388203ba002e0db72927ba0b1940c0..010d004ae5d74267ce607065533d3782a7b8e182 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 0000000000000000000000000000000000000000..5e9b0814fcbd78f5e72efd5f2546b3ff9ae163a5 --- /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