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