1 /*
2 * Copyright 2010-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 *
4 * Portions copyright 2006-2009 James Murty. Please see LICENSE.txt
5 * for applicable license terms and NOTICE.txt for applicable notices.
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License").
8 * You may not use this file except in compliance with the License.
9 * A copy of the License is located at
10 *
11 * http://aws.amazon.com/apache2.0
12 *
13 * or in the "license" file accompanying this file. This file is distributed
14 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
15 * express or implied. See the License for the specific language governing
16 * permissions and limitations under the License.
17 */
18 package com.amazonaws.util;
19
20 import java.math.BigDecimal;
21 import java.util.Date;
22 import java.util.Locale;
23 import java.util.concurrent.TimeUnit;
24
25 import org.joda.time.DateTime;
26 import org.joda.time.DateTimeZone;
27 import org.joda.time.format.DateTimeFormat;
28 import org.joda.time.format.DateTimeFormatter;
29 import org.joda.time.format.ISODateTimeFormat;
30 import org.joda.time.tz.FixedDateTimeZone;
31
32 import com.amazonaws.SdkClientException;
33 import com.amazonaws.annotation.ThreadSafe;
34
35 /**
36 * Utilities for parsing and formatting dates.
37 */
38 @ThreadSafe
39 public class DateUtils {
40 private static final DateTimeZone GMT = new FixedDateTimeZone("GMT", "GMT", 0, 0);
41 private static final long MILLI_SECONDS_OF_365_DAYS = 365L*24*60*60*1000;
42
43 private static final int AWS_DATE_MILLI_SECOND_PRECISION = 3;
44
45 /** ISO 8601 format */
46 protected static final DateTimeFormatter iso8601DateFormat =
47 ISODateTimeFormat.dateTime().withZone(GMT);
48
49 /** Alternate ISO 8601 format without fractional seconds */
50 protected static final DateTimeFormatter alternateIso8601DateFormat =
51 DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(GMT);
52
53 /** RFC 822 format */
54 protected static final DateTimeFormatter rfc822DateFormat =
55 DateTimeFormat.forPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'")
56 .withLocale(Locale.US)
57 .withZone(GMT);
58
59 /**
60 * This is another ISO 8601 format that's used in clock skew error response
61 */
62 protected static final DateTimeFormatter compressedIso8601DateFormat =
63 DateTimeFormat.forPattern("yyyyMMdd'T'HHmmss'Z'")
64 .withZone(GMT);
65
66 /**
67 * Parses the specified date string as an ISO 8601 date and returns the Date
68 * object.
69 *
70 * @param dateString
71 * The date string to parse.
72 *
73 * @return The parsed Date object.
74 */
75 public static Date parseISO8601Date(String dateString) {
76 try {
77 return doParseISO8601Date(dateString);
78 } catch(RuntimeException ex) {
79 throw handleException(ex);
80 }
81 }
82
83 static Date doParseISO8601Date(final String dateStringOrig) {
84 String dateString = dateStringOrig;
85
86 // For EC2 Spot Fleet.
87 if (dateString.endsWith("+0000")) {
88 dateString = dateString
89 .substring(0, dateString.length() - 5)
90 .concat("Z");
91 }
92
93 // https://github.com/aws/aws-sdk-java/issues/233
94 String temp = tempDateStringForJodaTime(dateString);
95 try {
96 if (temp.equals(dateString)) {
97 // Normal case: nothing special here
98 return new Date(iso8601DateFormat.parseMillis(dateString));
99 }
100 // Handling edge case:
101 // Joda-time can only handle up to year 292278993 but we are given
102 // 292278994; So we parse the date string by first adjusting
103 // the year to 292278993. Then we add 1 year back afterwards.
104 final long milliLess365Days = iso8601DateFormat.parseMillis(temp);
105 final long milli = milliLess365Days + MILLI_SECONDS_OF_365_DAYS;
106 if (milli < 0) { // overflow!
107 // re-parse the original date string using JodaTime so as to
108 // throw an exception with a consistent message
109 return new Date(iso8601DateFormat.parseMillis(dateString));
110 }
111 return new Date(milli);
112 } catch (IllegalArgumentException e) {
113 try {
114 return new Date(alternateIso8601DateFormat.parseMillis(dateString));
115 // If the first ISO 8601 parser didn't work, try the alternate
116 // version which doesn't include fractional seconds
117 } catch(Exception oops) {
118 // no the alternative route doesn't work; let's bubble up the original exception
119 throw e;
120 }
121 }
122 }
123
124 /**
125 * Returns a date string with the prefix temporarily substituted, if
126 * applicable, so that JodaTime can handle it. Otherwise, if not applicable,
127 * the original date string is returned.
128 * <p>
129 * See https://github.com/aws/aws-sdk-java/issues/233
130 */
131 private static String tempDateStringForJodaTime(String dateString) {
132 final String fromPrefix = "292278994-";
133 final String toPrefix = "292278993-";
134 return dateString.startsWith(fromPrefix)
135 ? toPrefix + dateString.substring(fromPrefix.length())
136 : dateString;
137 }
138
139 /**
140 * Returns the original runtime exception iff the joda-time being used
141 * at runtime behaves as expected.
142 *
143 * @throws IllegalStateException if the joda-time being used at runtime
144 * doens't appear to be of the right version.
145 */
146 private static <E extends RuntimeException> E handleException(E ex) {
147 if (JodaTime.hasExpectedBehavior())
148 return ex;
149 throw new IllegalStateException("Joda-time 2.2 or later version is required, but found version: " + JodaTime.getVersion(), ex);
150 }
151
152 /**
153 * Formats the specified date as an ISO 8601 string.
154 *
155 * @param date
156 * The date to format.
157 *
158 * @return The ISO 8601 string representing the specified date.
159 */
160 public static String formatISO8601Date(Date date) {
161 try {
162 return iso8601DateFormat.print(date.getTime());
163 } catch(RuntimeException ex) {
164 throw handleException(ex);
165 }
166 }
167
168 /**
169 * Formats the specified date as an ISO 8601 string.
170 *
171 * @param date the date to format
172 * @return the ISO-8601 string representing the specified date
173 */
174 public static String formatISO8601Date(DateTime date) {
175 try {
176 return iso8601DateFormat.print(date);
177 } catch (RuntimeException ex) {
178 throw handleException(ex);
179 }
180 }
181
182 /**
183 * Parses the specified date string as an RFC 822 date and returns the Date
184 * object.
185 *
186 * @param dateString
187 * The date string to parse.
188 *
189 * @return The parsed Date object.
190 */
191 public static Date parseRFC822Date(String dateString) {
192 if (dateString == null) {
193 return null;
194 }
195 try {
196 return new Date(rfc822DateFormat.parseMillis(dateString));
197 } catch(RuntimeException ex) {
198 throw handleException(ex);
199 }
200 }
201
202 /**
203 * Formats the specified date as an RFC 822 string.
204 *
205 * @param date
206 * The date to format.
207 *
208 * @return The RFC 822 string representing the specified date.
209 */
210 public static String formatRFC822Date(Date date) {
211 try {
212 return rfc822DateFormat.print(date.getTime());
213 } catch(RuntimeException ex) {
214 throw handleException(ex);
215 }
216 }
217
218 /**
219 * Parses the specified date string as a compressedIso8601DateFormat ("yyyyMMdd'T'HHmmss'Z'") and returns the Date
220 * object.
221 *
222 * @param dateString
223 * The date string to parse.
224 *
225 * @return The parsed Date object.
226 */
227 public static Date parseCompressedISO8601Date(String dateString) {
228 try {
229 return new Date(compressedIso8601DateFormat.parseMillis(dateString));
230 } catch (RuntimeException ex) {
231 throw handleException(ex);
232 }
233 }
234
235 /**
236 * Parses the given date string returned by the AWS service into a Date
237 * object.
238 */
239 public static Date parseServiceSpecificDate(String dateString) {
240 if (dateString == null)
241 return null;
242 try {
243 BigDecimal dateValue = new BigDecimal(dateString);
244 return new Date(dateValue.scaleByPowerOfTen(
245 AWS_DATE_MILLI_SECOND_PRECISION).longValue());
246 } catch (NumberFormatException nfe) {
247 throw new SdkClientException("Unable to parse date : "
248 + dateString, nfe);
249 }
250 }
251
252 public static Date parseUnixTimestampInMillis(String dateString) {
253 if (dateString == null)
254 return null;
255 try {
256 BigDecimal dateValue = new BigDecimal(dateString);
257 return new Date(dateValue.longValue());
258 } catch (NumberFormatException nfe) {
259 throw new SdkClientException("Unable to parse date : "
260 + dateString, nfe);
261 }
262 }
263
264 /**
265 * Formats the give date object into an AWS Service format.
266 */
267 public static String formatServiceSpecificDate(Date date) {
268 if (date == null)
269 return null;
270 BigDecimal dateValue = BigDecimal.valueOf(date.getTime());
271 return dateValue.scaleByPowerOfTen(0 - AWS_DATE_MILLI_SECOND_PRECISION)
272 .toPlainString();
273 }
274
275 /**
276 * Formats the give date object into unit timestamp in milli seconds.
277 */
278 public static String formatUnixTimestampInMills(Date date) {
279 if (date == null)
280 return null;
281 BigDecimal dateValue = BigDecimal.valueOf(date.getTime());
282 return dateValue.toPlainString();
283 }
284
285 public static Date cloneDate(Date date) {
286 return date == null ? null : new Date(date.getTime());
287 }
288
289 /**
290 * Returns the number of days since epoch with respect to the given number
291 * of milliseconds since epoch.
292 */
293 public static long numberOfDaysSinceEpoch(long milliSinceEpoch) {
294 return TimeUnit.MILLISECONDS.toDays(milliSinceEpoch);
295 }
296 }
297