1 /* 2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"). 5 * You may not use this file except in compliance with the License. 6 * A copy of the License is located at 7 * 8 * http://aws.amazon.com/apache2.0 9 * 10 * or in the "license" file accompanying this file. This file is distributed 11 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 * express or implied. See the License for the specific language governing 13 * permissions and limitations under the License. 14 */ 15 16 package software.amazon.awssdk.utils; 17 18 import static java.time.ZoneOffset.UTC; 19 import static java.time.format.DateTimeFormatter.ISO_INSTANT; 20 import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME; 21 import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME; 22 import static org.assertj.core.api.Assertions.assertThat; 23 import static org.assertj.core.api.Assertions.assertThatThrownBy; 24 import static org.junit.Assert.assertEquals; 25 import static org.junit.Assert.assertTrue; 26 import static software.amazon.awssdk.utils.DateUtils.ALTERNATE_ISO_8601_DATE_FORMAT; 27 import static software.amazon.awssdk.utils.DateUtils.RFC_822_DATE_TIME; 28 29 import java.text.ParseException; 30 import java.text.SimpleDateFormat; 31 import java.time.Duration; 32 import java.time.Instant; 33 import java.time.OffsetDateTime; 34 import java.time.ZoneId; 35 import java.time.ZonedDateTime; 36 import java.time.format.DateTimeFormatter; 37 import java.time.format.DateTimeParseException; 38 import java.util.Arrays; 39 import java.util.Date; 40 import java.util.Locale; 41 import java.util.TimeZone; 42 import java.util.concurrent.TimeUnit; 43 import java.util.stream.Collectors; 44 import java.util.stream.Stream; 45 import org.junit.Test; 46 47 public class DateUtilsTest { 48 private static final boolean DEBUG = false; 49 private static final int MAX_MILLIS_YEAR = 292278994; 50 private static final SimpleDateFormat COMMON_DATE_FORMAT = 51 new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); 52 private static final SimpleDateFormat LONG_DATE_FORMAT = 53 new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US); 54 55 static { TimeZone.getTimeZone(UTC)56 COMMON_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone(UTC)); TimeZone.getTimeZone(UTC)57 LONG_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone(UTC)); 58 } 59 60 private static final Instant INSTANT = Instant.ofEpochMilli(1400284606000L); 61 62 @Test tt0031561767()63 public void tt0031561767() { 64 String input = "Fri, 16 May 2014 23:56:46 GMT"; 65 Instant instant = DateUtils.parseRfc1123Date(input); 66 assertEquals(input, DateUtils.formatRfc1123Date(instant)); 67 } 68 69 @Test formatIso8601Date()70 public void formatIso8601Date() throws ParseException { 71 Date date = Date.from(INSTANT); 72 String expected = COMMON_DATE_FORMAT.format(date); 73 String actual = DateUtils.formatIso8601Date(date.toInstant()); 74 assertEquals(expected, actual); 75 76 Instant expectedDate = COMMON_DATE_FORMAT.parse(expected).toInstant(); 77 Instant actualDate = DateUtils.parseIso8601Date(actual); 78 assertEquals(expectedDate, actualDate); 79 } 80 81 @Test formatRfc822Date_DateWithTwoDigitDayOfMonth_ReturnsFormattedString()82 public void formatRfc822Date_DateWithTwoDigitDayOfMonth_ReturnsFormattedString() throws ParseException { 83 String string = DateUtils.formatRfc822Date(INSTANT); 84 Instant parsedDateAsInstant = LONG_DATE_FORMAT.parse(string).toInstant(); 85 assertThat(parsedDateAsInstant).isEqualTo(INSTANT); 86 } 87 88 @Test formatRfc822Date_DateWithSingleDigitDayOfMonth_ReturnsFormattedString()89 public void formatRfc822Date_DateWithSingleDigitDayOfMonth_ReturnsFormattedString() throws ParseException { 90 Instant INSTANT_SINGLE_DIGIT_DAY_OF_MONTH = Instant.ofEpochMilli(1399484606000L);; 91 String string = DateUtils.formatRfc822Date(INSTANT_SINGLE_DIGIT_DAY_OF_MONTH); 92 Instant parsedDateAsInstant = LONG_DATE_FORMAT.parse(string).toInstant(); 93 assertThat(parsedDateAsInstant).isEqualTo(INSTANT_SINGLE_DIGIT_DAY_OF_MONTH); 94 } 95 96 @Test formatRfc822Date_DateWithSingleDigitDayOfMonth_ReturnsStringWithZeroLeadingDayOfMonth()97 public void formatRfc822Date_DateWithSingleDigitDayOfMonth_ReturnsStringWithZeroLeadingDayOfMonth() throws ParseException { 98 final Instant INSTANT_SINGLE_DIGIT_DAY_OF_MONTH = Instant.ofEpochMilli(1399484606000L);; 99 String string = DateUtils.formatRfc822Date(INSTANT_SINGLE_DIGIT_DAY_OF_MONTH); 100 String expectedString = "Wed, 07 May 2014 17:43:26 GMT"; 101 assertThat(string).isEqualTo(expectedString); 102 } 103 104 @Test parseRfc822Date_DateWithTwoDigitDayOfMonth_ReturnsInstantObject()105 public void parseRfc822Date_DateWithTwoDigitDayOfMonth_ReturnsInstantObject() throws ParseException { 106 String formattedDate = LONG_DATE_FORMAT.format(Date.from(INSTANT)); 107 Instant parsedInstant = DateUtils.parseRfc822Date(formattedDate); 108 assertThat(parsedInstant).isEqualTo(INSTANT); 109 } 110 111 @Test parseRfc822Date_DateWithSingleDigitDayOfMonth_ReturnsInstantObject()112 public void parseRfc822Date_DateWithSingleDigitDayOfMonth_ReturnsInstantObject() throws ParseException { 113 final Instant INSTANT_SINGLE_DIGIT_DAY_OF_MONTH = Instant.ofEpochMilli(1399484606000L);; 114 String formattedDate = LONG_DATE_FORMAT.format(Date.from(INSTANT_SINGLE_DIGIT_DAY_OF_MONTH)); 115 Instant parsedInstant = DateUtils.parseRfc822Date(formattedDate); 116 assertThat(parsedInstant).isEqualTo(INSTANT_SINGLE_DIGIT_DAY_OF_MONTH); 117 } 118 119 @Test parseRfc822Date_DateWithInvalidDayOfMonth_IsParsedWithSmartResolverStyle()120 public void parseRfc822Date_DateWithInvalidDayOfMonth_IsParsedWithSmartResolverStyle() { 121 String badDateString = "Wed, 31 Apr 2014 17:43:26 GMT"; 122 String validDateString = "Wed, 30 Apr 2014 17:43:26 GMT"; 123 Instant badDateParsedInstant = DateUtils.parseRfc822Date(badDateString); 124 Instant validDateParsedInstant = DateUtils.parseRfc1123Date(validDateString); 125 assertThat(badDateParsedInstant).isEqualTo(validDateParsedInstant); 126 } 127 128 @Test parseRfc822Date_DateWithInvalidDayOfMonth_MatchesRfc1123Behavior()129 public void parseRfc822Date_DateWithInvalidDayOfMonth_MatchesRfc1123Behavior() { 130 String dateString = "Wed, 31 Apr 2014 17:43:26 GMT"; 131 Instant parsedInstantFromRfc822Parser = DateUtils.parseRfc822Date(dateString); 132 Instant parsedInstantFromRfc1123arser = DateUtils.parseRfc1123Date(dateString); 133 assertThat(parsedInstantFromRfc822Parser).isEqualTo(parsedInstantFromRfc1123arser); 134 } 135 136 @Test parseRfc822Date_DateWithDayOfMonthLessThan10th_MatchesRfc1123Behavior()137 public void parseRfc822Date_DateWithDayOfMonthLessThan10th_MatchesRfc1123Behavior() { 138 String rfc822DateString = "Wed, 02 Apr 2014 17:43:26 GMT"; 139 String rfc1123DateString = "Wed, 2 Apr 2014 17:43:26 GMT"; 140 Instant parsedInstantFromRfc822Parser = DateUtils.parseRfc822Date(rfc822DateString); 141 Instant parsedInstantFromRfc1123arser = DateUtils.parseRfc1123Date(rfc1123DateString); 142 assertThat(parsedInstantFromRfc822Parser).isEqualTo(parsedInstantFromRfc1123arser); 143 } 144 145 @Test resolverStyleOfRfc822FormatterMatchesRfc1123Formatter()146 public void resolverStyleOfRfc822FormatterMatchesRfc1123Formatter() { 147 assertThat(RFC_822_DATE_TIME.getResolverStyle()).isSameAs(RFC_1123_DATE_TIME.getResolverStyle()); 148 } 149 150 @Test chronologyOfRfc822FormatterMatchesRfc1123Formatter()151 public void chronologyOfRfc822FormatterMatchesRfc1123Formatter() { 152 assertThat(RFC_822_DATE_TIME.getChronology()).isSameAs(RFC_1123_DATE_TIME.getChronology()); 153 } 154 155 @Test formatRfc1123Date()156 public void formatRfc1123Date() throws ParseException { 157 String string = DateUtils.formatRfc1123Date(INSTANT); 158 Instant parsedDateAsInstant = LONG_DATE_FORMAT.parse(string).toInstant(); 159 assertEquals(INSTANT, parsedDateAsInstant); 160 161 String formattedDate = LONG_DATE_FORMAT.format(Date.from(INSTANT)); 162 Instant parsedInstant = DateUtils.parseRfc1123Date(formattedDate); 163 assertEquals(INSTANT, parsedInstant); 164 } 165 166 @Test parseRfc1123Date()167 public void parseRfc1123Date() throws ParseException { 168 String formatted = LONG_DATE_FORMAT.format(Date.from(INSTANT)); 169 Instant expected = LONG_DATE_FORMAT.parse(formatted).toInstant(); 170 Instant actual = DateUtils.parseRfc1123Date(formatted); 171 assertEquals(expected, actual); 172 } 173 174 @Test parseIso8601Date()175 public void parseIso8601Date() throws ParseException { 176 checkParsing(DateTimeFormatter.ISO_INSTANT, COMMON_DATE_FORMAT); 177 } 178 179 @Test parseIso8601Date_withUtcOffset()180 public void parseIso8601Date_withUtcOffset() { 181 String formatted = "2021-05-10T17:12:13-07:00"; 182 Instant expected = ISO_OFFSET_DATE_TIME.parse(formatted, Instant::from); 183 Instant actual = DateUtils.parseIso8601Date(formatted); 184 assertEquals(expected, actual); 185 186 String actualString = OffsetDateTime.ofInstant(actual, ZoneId.of("-7")).toString(); 187 assertEquals(formatted, actualString); 188 } 189 190 @Test parseIso8601Date_usingAlternativeFormat()191 public void parseIso8601Date_usingAlternativeFormat() throws ParseException { 192 checkParsing(ALTERNATE_ISO_8601_DATE_FORMAT, COMMON_DATE_FORMAT); 193 } 194 checkParsing(DateTimeFormatter dateTimeFormatter, SimpleDateFormat dateFormat)195 private void checkParsing(DateTimeFormatter dateTimeFormatter, SimpleDateFormat dateFormat) throws ParseException { 196 String formatted = dateFormat.format(Date.from(INSTANT)); 197 String alternative = dateTimeFormatter.format(INSTANT); 198 assertEquals(formatted, alternative); 199 Instant expected = dateFormat.parse(formatted).toInstant(); 200 Instant actualDate = DateUtils.parseIso8601Date(formatted); 201 assertEquals(expected, actualDate); 202 } 203 204 @Test alternateIso8601DateFormat()205 public void alternateIso8601DateFormat() throws ParseException { 206 String expected = COMMON_DATE_FORMAT.format(Date.from(INSTANT)); 207 String actual = ALTERNATE_ISO_8601_DATE_FORMAT.format(INSTANT); 208 assertEquals(expected, actual); 209 210 Date expectedDate = COMMON_DATE_FORMAT.parse(expected); 211 ZonedDateTime actualDateTime = ZonedDateTime.parse(actual, ALTERNATE_ISO_8601_DATE_FORMAT); 212 assertEquals(expectedDate, Date.from(actualDateTime.toInstant())); 213 } 214 215 @Test(expected = ParseException.class) legacyHandlingOfInvalidDate()216 public void legacyHandlingOfInvalidDate() throws ParseException { 217 final String input = "2014-03-06T14:28:58.000Z.000Z"; 218 COMMON_DATE_FORMAT.parse(input); 219 } 220 221 @Test(expected = DateTimeParseException.class) invalidDate()222 public void invalidDate() { 223 final String input = "2014-03-06T14:28:58.000Z.000Z"; 224 DateUtils.parseIso8601Date(input); 225 } 226 227 @Test testIssue233()228 public void testIssue233() throws ParseException { 229 // https://github.com/aws/aws-sdk-java/issues/233 230 final String edgeCase = String.valueOf(MAX_MILLIS_YEAR) + "-08-17T07:12:00Z"; 231 Instant expected = COMMON_DATE_FORMAT.parse(edgeCase).toInstant(); 232 if (DEBUG) { 233 System.out.println("date: " + expected); 234 } 235 String formatted = DateUtils.formatIso8601Date(expected); 236 if (DEBUG) { 237 System.out.println("formatted: " + formatted); 238 } 239 // we have '+' sign as prefix for years. See java.time.format.SignStyle.EXCEEDS_PAD 240 assertEquals(edgeCase, formatted.substring(1)); 241 242 Instant parsed = DateUtils.parseIso8601Date(formatted); 243 if (DEBUG) { 244 System.out.println("parsed: " + parsed); 245 } 246 assertEquals(expected, parsed); 247 String reformatted = ISO_INSTANT.format(parsed); 248 // we have '+' sign as prefix for years. See java.time.format.SignStyle.EXCEEDS_PAD 249 assertEquals(edgeCase, reformatted.substring(1)); 250 } 251 252 @Test testIssue233JavaTimeLimit()253 public void testIssue233JavaTimeLimit() { 254 // https://github.com/aws/aws-sdk-java/issues/233 255 String s = ALTERNATE_ISO_8601_DATE_FORMAT.format( 256 ZonedDateTime.ofInstant(Instant.ofEpochMilli(Long.MAX_VALUE), UTC)); 257 System.out.println("s: " + s); 258 259 Instant parsed = DateUtils.parseIso8601Date(s); 260 assertEquals(ZonedDateTime.ofInstant(parsed, UTC).getYear(), MAX_MILLIS_YEAR); 261 } 262 263 @Test testIssueDaysDiff()264 public void testIssueDaysDiff() throws ParseException { 265 // https://github.com/aws/aws-sdk-java/issues/233 266 COMMON_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone(UTC)); 267 String edgeCase = String.valueOf(MAX_MILLIS_YEAR) + "-08-17T07:12:55Z"; 268 String testCase = String.valueOf(MAX_MILLIS_YEAR - 1) + "-08-17T07:12:55Z"; 269 Date edgeDate = COMMON_DATE_FORMAT.parse(edgeCase); 270 Date testDate = COMMON_DATE_FORMAT.parse(testCase); 271 long diff = edgeDate.getTime() - testDate.getTime(); 272 assertTrue(diff == TimeUnit.DAYS.toMillis(365)); 273 } 274 275 @Test numberOfDaysSinceEpoch()276 public void numberOfDaysSinceEpoch() { 277 final long now = System.currentTimeMillis(); 278 final long days = DateUtils.numberOfDaysSinceEpoch(now); 279 final long oneDayMilli = Duration.ofDays(1).toMillis(); 280 // Could be equal at 00:00:00. 281 assertTrue(now >= Duration.ofDays(days).toMillis()); 282 assertTrue((now - Duration.ofDays(days).toMillis()) <= oneDayMilli); 283 } 284 285 /** 286 * Tests the Date marshalling and unmarshalling. Asserts that the value is 287 * same before and after marshalling/unmarshalling 288 */ 289 @Test testUnixTimestampRoundtrip()290 public void testUnixTimestampRoundtrip() throws Exception { 291 long[] testValues = new long[] {System.currentTimeMillis(), 1L, 0L}; 292 Arrays.stream(testValues) 293 .mapToObj(Instant::ofEpochMilli) 294 .forEach(instant -> { 295 String serverSpecificDateFormat = DateUtils.formatUnixTimestampInstant(instant); 296 Instant parsed = DateUtils.parseUnixTimestampInstant(String.valueOf(serverSpecificDateFormat)); 297 assertEquals(instant, parsed); 298 }); 299 } 300 301 @Test parseUnixTimestampInstant_longerThan20Char_throws()302 public void parseUnixTimestampInstant_longerThan20Char_throws() { 303 String largeNum = Stream.generate(() -> "9").limit(21).collect(Collectors.joining()); 304 assertThatThrownBy(() -> DateUtils.parseUnixTimestampInstant(largeNum)) 305 .isInstanceOf(RuntimeException.class) 306 .hasMessageContaining("20"); 307 } 308 309 } 310