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 23 import java.math.BigDecimal; 24 import java.time.Duration; 25 import java.time.Instant; 26 import java.time.ZoneOffset; 27 import java.time.ZonedDateTime; 28 import java.time.chrono.IsoChronology; 29 import java.time.format.DateTimeFormatter; 30 import java.time.format.DateTimeFormatterBuilder; 31 import java.time.format.DateTimeParseException; 32 import java.time.format.ResolverStyle; 33 import java.util.Arrays; 34 import java.util.List; 35 import java.util.Locale; 36 import software.amazon.awssdk.annotations.SdkProtectedApi; 37 import software.amazon.awssdk.annotations.ThreadSafe; 38 39 /** 40 * Utilities for parsing and formatting dates. 41 */ 42 @ThreadSafe 43 @SdkProtectedApi 44 public final class DateUtils { 45 /** 46 * Alternate ISO 8601 format without fractional seconds. 47 */ 48 static final DateTimeFormatter ALTERNATE_ISO_8601_DATE_FORMAT = 49 new DateTimeFormatterBuilder() 50 .appendPattern("yyyy-MM-dd'T'HH:mm:ss'Z'") 51 .toFormatter() 52 .withZone(UTC); 53 54 /** 55 * RFC 822 date/time formatter. 56 */ 57 static final DateTimeFormatter RFC_822_DATE_TIME = new DateTimeFormatterBuilder() 58 .parseCaseInsensitive() 59 .parseLenient() 60 .appendPattern("EEE, dd MMM yyyy HH:mm:ss") 61 .appendLiteral(' ') 62 .appendOffset("+HHMM", "GMT") 63 .toFormatter() 64 .withLocale(Locale.US) 65 .withResolverStyle(ResolverStyle.SMART) 66 .withChronology(IsoChronology.INSTANCE); 67 68 // ISO_INSTANT does not handle offsets in Java 12-. See https://bugs.openjdk.java.net/browse/JDK-8166138 69 private static final List<DateTimeFormatter> ALTERNATE_ISO_8601_FORMATTERS = 70 Arrays.asList(ISO_INSTANT, ALTERNATE_ISO_8601_DATE_FORMAT, ISO_OFFSET_DATE_TIME); 71 72 private static final int MILLI_SECOND_PRECISION = 3; 73 DateUtils()74 private DateUtils() { 75 } 76 77 /** 78 * Parses the specified date string as an ISO 8601 date (yyyy-MM-dd'T'HH:mm:ss.SSSZZ) 79 * and returns the {@link Instant} object. 80 * 81 * @param dateString 82 * The date string to parse. 83 * 84 * @return The parsed Instant object. 85 */ parseIso8601Date(String dateString)86 public static Instant parseIso8601Date(String dateString) { 87 // For EC2 Spot Fleet. 88 if (dateString.endsWith("+0000")) { 89 dateString = dateString 90 .substring(0, dateString.length() - 5) 91 .concat("Z"); 92 } 93 94 DateTimeParseException exception = null; 95 96 for (DateTimeFormatter formatter : ALTERNATE_ISO_8601_FORMATTERS) { 97 try { 98 return parseInstant(dateString, formatter); 99 } catch (DateTimeParseException e) { 100 exception = e; 101 } 102 } 103 104 if (exception != null) { 105 throw exception; 106 } 107 108 // should never execute this 109 throw new RuntimeException("Failed to parse date " + dateString); 110 } 111 112 /** 113 * Formats the specified date as an ISO 8601 string. 114 * 115 * @param date the date to format 116 * @return the ISO-8601 string representing the specified date 117 */ formatIso8601Date(Instant date)118 public static String formatIso8601Date(Instant date) { 119 return ISO_INSTANT.format(date); 120 } 121 122 /** 123 * Parses the specified date string as an RFC 822 date and returns the Date object. 124 * 125 * @param dateString 126 * The date string to parse. 127 * 128 * @return The parsed Date object. 129 */ parseRfc822Date(String dateString)130 public static Instant parseRfc822Date(String dateString) { 131 if (dateString == null) { 132 return null; 133 } 134 return parseInstant(dateString, RFC_822_DATE_TIME); 135 } 136 137 /** 138 * Formats the specified date as an RFC 822 string. 139 * 140 * @param instant 141 * The instant to format. 142 * 143 * @return The RFC 822 string representing the specified date. 144 */ formatRfc822Date(Instant instant)145 public static String formatRfc822Date(Instant instant) { 146 return RFC_822_DATE_TIME.format(ZonedDateTime.ofInstant(instant, UTC)); 147 } 148 149 /** 150 * Parses the specified date string as an RFC 1123 date and returns the Date 151 * object. 152 * 153 * @param dateString 154 * The date string to parse. 155 * 156 * @return The parsed Date object. 157 */ parseRfc1123Date(String dateString)158 public static Instant parseRfc1123Date(String dateString) { 159 if (dateString == null) { 160 return null; 161 } 162 return parseInstant(dateString, RFC_1123_DATE_TIME); 163 } 164 165 /** 166 * Formats the specified date as an RFC 1123 string. 167 * 168 * @param instant 169 * The instant to format. 170 * 171 * @return The RFC 1123 string representing the specified date. 172 */ formatRfc1123Date(Instant instant)173 public static String formatRfc1123Date(Instant instant) { 174 return RFC_1123_DATE_TIME.format(ZonedDateTime.ofInstant(instant, UTC)); 175 } 176 177 /** 178 * Returns the number of days since epoch with respect to the given number 179 * of milliseconds since epoch. 180 */ numberOfDaysSinceEpoch(long milliSinceEpoch)181 public static long numberOfDaysSinceEpoch(long milliSinceEpoch) { 182 return Duration.ofMillis(milliSinceEpoch).toDays(); 183 } 184 parseInstant(String dateString, DateTimeFormatter formatter)185 private static Instant parseInstant(String dateString, DateTimeFormatter formatter) { 186 187 // Should not call formatter.withZone(ZoneOffset.UTC) because it will override the zone 188 // for timestamps with an offset. See https://bugs.openjdk.java.net/browse/JDK-8177021 189 if (formatter.equals(ISO_OFFSET_DATE_TIME)) { 190 return formatter.parse(dateString, Instant::from); 191 } 192 193 return formatter.withZone(ZoneOffset.UTC).parse(dateString, Instant::from); 194 } 195 196 /** 197 * Parses the given string containing a Unix timestamp with millisecond decimal precision into an {@link Instant} object. 198 */ parseUnixTimestampInstant(String dateString)199 public static Instant parseUnixTimestampInstant(String dateString) throws NumberFormatException { 200 if (dateString == null) { 201 return null; 202 } 203 204 validateTimestampLength(dateString); 205 BigDecimal dateValue = new BigDecimal(dateString); 206 return Instant.ofEpochMilli(dateValue.scaleByPowerOfTen(MILLI_SECOND_PRECISION).longValue()); 207 } 208 209 /** 210 * Parses the given string containing a Unix timestamp in epoch millis into a {@link Instant} object. 211 */ parseUnixTimestampMillisInstant(String dateString)212 public static Instant parseUnixTimestampMillisInstant(String dateString) throws NumberFormatException { 213 if (dateString == null) { 214 return null; 215 } 216 return Instant.ofEpochMilli(Long.parseLong(dateString)); 217 } 218 219 /** 220 * Formats the give {@link Instant} object into an Unix timestamp with millisecond decimal precision. 221 */ formatUnixTimestampInstant(Instant instant)222 public static String formatUnixTimestampInstant(Instant instant) { 223 if (instant == null) { 224 return null; 225 } 226 BigDecimal dateValue = BigDecimal.valueOf(instant.toEpochMilli()); 227 return dateValue.scaleByPowerOfTen(0 - MILLI_SECOND_PRECISION) 228 .toPlainString(); 229 } 230 validateTimestampLength(String timestamp)231 private static void validateTimestampLength(String timestamp) { 232 // Helps avoid BigDecimal parsing unnecessarily large numbers, since it's unbounded 233 // Long has a max value of 9,223,372,036,854,775,807, which is 19 digits. Assume that a valid timestamp is no 234 // no longer than 20 characters long (+1 for decimal) 235 if (timestamp.length() > 20) { 236 throw new RuntimeException("Input timestamp string must be no longer than 20 characters"); 237 } 238 } 239 } 240