xref: /aosp_15_r20/external/aws-sdk-java-v2/utils/src/main/java/software/amazon/awssdk/utils/DateUtils.java (revision 8a52c7834d808308836a99fc2a6e0ed8db339086)
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