001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *      http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    package org.apache.oozie.util;
019    
020    import java.sql.Timestamp;
021    import java.text.DateFormat;
022    import java.text.ParseException;
023    import java.text.ParsePosition;
024    import java.text.SimpleDateFormat;
025    import java.util.Calendar;
026    import java.util.Date;
027    import java.util.GregorianCalendar;
028    import java.util.TimeZone;
029    import java.util.regex.Matcher;
030    import java.util.regex.Pattern;
031    
032    import org.apache.hadoop.conf.Configuration;
033    import org.apache.oozie.coord.TimeUnit;
034    
035    /**
036     * Date utility classes to parse and format datetimes in Oozie expected datetime formats.
037     */
038    public class DateUtils {
039    
040        private static final Pattern GMT_OFFSET_COLON_PATTERN = Pattern.compile("^GMT(\\-|\\+)(\\d{2})(\\d{2})$");
041    
042        public static final TimeZone UTC = getTimeZone("UTC");
043    
044        public static final String ISO8601_UTC_MASK = "yyyy-MM-dd'T'HH:mm'Z'";
045        private static final String ISO8601_TZ_MASK_WITHOUT_OFFSET = "yyyy-MM-dd'T'HH:mm";
046    
047        private static String ACTIVE_MASK = ISO8601_UTC_MASK;
048        private static TimeZone ACTIVE_TIMEZONE = UTC;
049    
050        public static final String OOZIE_PROCESSING_TIMEZONE_KEY = "oozie.processing.timezone";
051    
052        public static final String OOZIE_PROCESSING_TIMEZONE_DEFAULT = "UTC";
053    
054        private static boolean OOZIE_IN_UTC = true;
055    
056        private static final Pattern VALID_TIMEZONE_PATTERN = Pattern.compile("^UTC$|^GMT(\\+|\\-)\\d{4}$");
057    
058        /**
059         * Configures the Datetime parsing with Oozie processing timezone.
060         * <p/>
061         * The {@link #OOZIE_PROCESSING_TIMEZONE_KEY} property is read and set as the Oozie processing timezone.
062         * Valid values for this property are <code>UTC</code> and <code>GMT(+/-)####</code>
063         *
064         * @param conf Oozie server configuration.
065         */
066        public static void setConf(Configuration conf) {
067            String tz = conf.get(OOZIE_PROCESSING_TIMEZONE_KEY, OOZIE_PROCESSING_TIMEZONE_DEFAULT);
068            if (!VALID_TIMEZONE_PATTERN.matcher(tz).matches()) {
069                throw new RuntimeException("Invalid Oozie timezone, it must be 'UTC' or 'GMT(+/-)####");
070            }
071            ACTIVE_TIMEZONE = TimeZone.getTimeZone(tz);
072            OOZIE_IN_UTC = ACTIVE_TIMEZONE.equals(UTC);
073            ACTIVE_MASK = (OOZIE_IN_UTC) ? ISO8601_UTC_MASK : ISO8601_TZ_MASK_WITHOUT_OFFSET + tz.substring(3);
074        }
075    
076        /**
077         * Returns Oozie processing timezone.
078         *
079         * @return Oozie processing timezone. The returned timezone is <code>UTC</code> or a <code>GMT(+/-)####</code>
080         * timezone.
081         */
082        public static TimeZone getOozieProcessingTimeZone() {
083            return ACTIVE_TIMEZONE;
084        }
085    
086        /**
087         * Returns Oozie processing datetime mask.
088         * <p/>
089         * This mask is an ISO8601 datetime mask for the Oozie processing timezone.
090         *
091         * @return  Oozie processing datetime mask.
092         */
093        public static String getOozieTimeMask() {
094            return ACTIVE_MASK;
095        }
096    
097        private static DateFormat getISO8601DateFormat(TimeZone tz, String mask) {
098            DateFormat dateFormat = new SimpleDateFormat(mask);
099            // Stricter parsing to prevent dates such as 2011-12-50T01:00Z (December 50th) from matching
100            dateFormat.setLenient(false);
101            dateFormat.setTimeZone(tz);
102            return dateFormat;
103        }
104    
105        private static DateFormat getSpecificDateFormat(String format) {
106            DateFormat dateFormat = new SimpleDateFormat(format);
107            dateFormat.setTimeZone(ACTIVE_TIMEZONE);
108            return dateFormat;
109        }
110    
111        /**
112         * {@link TimeZone#getTimeZone(java.lang.String)} takes the timezone ID as an argument; for invalid IDs it returns the
113         * <code>GMT</code> TimeZone.  A timezone ID formatted like <code>GMT-####</code> is not a valid ID, however, it will actually
114         * map this to the <code>GMT-##:##</code> TimeZone, instead of returning the <code>GMT</code> TimeZone.  We check (later)
115         * check that a timezone ID is valid by calling {@link TimeZone#getTimeZone(java.lang.String)} and seeing if the returned
116         * TimeZone ID is equal to the original; because we want to allow <code>GMT-####</code>, while still disallowing actual
117         * invalid IDs, we have to manually replace <code>GMT-####</code> with <code>GMT-##:##</code> first.
118         *
119         * @param tzId The timezone ID
120         * @return If tzId matches <code>GMT-####</code>, then we return <code>GMT-##:##</code>; otherwise, we return tzId unaltered
121         */
122        private static String handleGMTOffsetTZNames(String tzId) {
123            Matcher m = GMT_OFFSET_COLON_PATTERN.matcher(tzId);
124            if (m.matches() && m.groupCount() == 3) {
125                tzId = "GMT" + m.group(1) + m.group(2) + ":" + m.group(3);
126            }
127            return tzId;
128        }
129    
130        /**
131         * Returns the {@link TimeZone} for the given timezone ID.
132         *
133         * @param tzId timezone ID.
134         * @return  the {@link TimeZone} for the given timezone ID.
135         */
136        public static TimeZone getTimeZone(String tzId) {
137            if (tzId == null) {
138                throw new IllegalArgumentException("Invalid TimeZone: " + tzId);
139            }
140            tzId = handleGMTOffsetTZNames(tzId);    // account for GMT-####
141            TimeZone tz = TimeZone.getTimeZone(tzId);
142            // If these are not equal, it means that the tzId is not valid (invalid tzId's return GMT)
143            if (!tz.getID().equals(tzId)) {
144                throw new IllegalArgumentException("Invalid TimeZone: " + tzId);
145            }
146            return tz;
147        }
148    
149        /**
150         * Parses a datetime in ISO8601 format in UTC timezone
151         *
152         * @param s string with the datetime to parse.
153         * @return the corresponding {@link Date} instance for the parsed date.
154         * @throws ParseException thrown if the given string was not an ISO8601 UTC value.
155         */
156        public static Date parseDateUTC(String s) throws ParseException {
157            return getISO8601DateFormat(UTC, ISO8601_UTC_MASK).parse(s);
158        }
159    
160        /**
161         * Parses a datetime in ISO8601 format in the Oozie processing timezone.
162         *
163         * @param s string with the datetime to parse.
164         * @return the corresponding {@link Date} instance for the parsed date.
165         * @throws ParseException thrown if the given string was not an ISO8601 value for the Oozie processing timezon.
166         */
167        public static Date parseDateOozieTZ(String s) throws ParseException {
168            s = s.trim();
169            ParsePosition pos = new ParsePosition(0);
170            Date d = getISO8601DateFormat(ACTIVE_TIMEZONE, ACTIVE_MASK).parse(s, pos);
171            if (d == null) {
172                throw new ParseException("Could not parse [" + s + "] using [" + ACTIVE_MASK + "] mask",
173                                         pos.getErrorIndex());
174            }
175            if (d != null && s.length() > pos.getIndex()) {
176                throw new ParseException("Correct datetime string is followed by invalid characters: " + s, pos.getIndex());
177            }
178            return d;
179        }
180    
181        /**
182         * Formats a {@link Date} as a string in ISO8601 format using Oozie processing timezone.
183         *
184         * @param d {@link Date} to format.
185         * @return the ISO8601 string for the given date, <code>NULL</code> if the {@link Date} instance was
186         * <code>NULL</code>
187         */
188        public static String formatDateOozieTZ(Date d) {
189            return (d != null) ? getISO8601DateFormat(ACTIVE_TIMEZONE, ACTIVE_MASK).format(d) : "NULL";
190        }
191    
192        /**
193         * Formats a {@link Date} as a string using the specified format mask.
194         * <p/>
195         * The format mask must be a {@link SimpleDateFormat} valid format mask.
196         *
197         * @param d {@link Date} to format.
198         * @return the string for the given date using the specified format mask,
199         * <code>NULL</code> if the {@link Date} instance was <code>NULL</code>
200         */
201        public static String formatDateCustom(Date d, String format) {
202            return (d != null) ? getSpecificDateFormat(format).format(d) : "NULL";
203        }
204    
205        /**
206         * Formats a {@link Calendar} as a string in ISO8601 format using Oozie processing timezone.
207         *
208         * @param c {@link Calendar} to format.
209         * @return the ISO8601 string for the given date, <code>NULL</code> if the {@link Calendar} instance was
210         * <code>NULL</code>
211         */
212        public static String formatDateOozieTZ(Calendar c) {
213            return (c != null) ? formatDateOozieTZ(c.getTime()) : "NULL";
214        }
215    
216        /**
217         * This function returns number of hour in a day when given a Calendar with appropriate TZ. It consider DST to find
218         * the number of hours. Generally it is 24. At some tZ, in one day of a year it is 23 and another day it is 25
219         *
220         * @param cal: The date for which the number of hours is requested
221         * @return number of hour in that day.
222         */
223        public static int hoursInDay(Calendar cal) {
224            Calendar localCal = new GregorianCalendar(cal.getTimeZone());
225            localCal.set(Calendar.MILLISECOND, 0);
226            localCal.set(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), 0, 30, 0);
227            localCal.add(Calendar.HOUR_OF_DAY, 24);
228            switch (localCal.get(Calendar.HOUR_OF_DAY)) {
229                case 1:
230                    return 23;
231                case 23:
232                    return 25;
233                default: // Case 0
234                    return 24;
235            }
236        }
237    
238        /**
239         * Determine whether a specific date is on DST change day
240         *
241         * @param cal: Date to know if it is DST change day. Appropriate TZ is specified
242         * @return true , if it DST change date otherwise false
243         */
244        public static boolean isDSTChangeDay(Calendar cal) {
245            return hoursInDay(cal) != 24;
246        }
247    
248        /**
249         * Move the any date-time to the end of the duration. If endOfFlag == day, move the date to the end of day (24:00 on
250         * the same day or 00:00 on the next day) If endOf Flag = month. move the date to then end of current month
251         * Otherwise do nothing
252         *
253         * @param cal : Date-time needs to be moved to the end
254         * @param endOfFlag : day (for end of day) or month (for end of month) or empty
255         */
256        public static void moveToEnd(Calendar cal, TimeUnit endOfFlag) {
257            // TODO: Both logic needs to be checked
258            if (endOfFlag == TimeUnit.END_OF_DAY) { // 24:00:00
259                cal.add(Calendar.DAY_OF_MONTH, 1);
260                // cal.set(Calendar.HOUR_OF_DAY, cal
261                // .getActualMaximum(Calendar.HOUR_OF_DAY) + 1);// TODO:
262                cal.set(Calendar.HOUR_OF_DAY, 0);
263                cal.set(Calendar.MINUTE, 0);
264                cal.set(Calendar.SECOND, 0);
265            }
266            else {
267                if (endOfFlag == TimeUnit.END_OF_MONTH) {
268                    cal.add(Calendar.MONTH, 1);
269                    cal.set(Calendar.DAY_OF_MONTH, 1);
270                    cal.set(Calendar.HOUR_OF_DAY, 0);
271                    cal.set(Calendar.MINUTE, 0);
272                    cal.set(Calendar.SECOND, 0);
273                }
274            }
275        }
276    
277        /**
278         * Create a Calendar instance using the specified date and Time zone
279         * @param dateString
280         * @param tz : TimeZone
281         * @return appropriate Calendar object
282         * @throws Exception
283         */
284        public static Calendar getCalendar(String dateString, TimeZone tz) throws Exception {
285            Date date = DateUtils.parseDateOozieTZ(dateString);
286            Calendar calDate = Calendar.getInstance();
287            calDate.setTime(date);
288            calDate.setTimeZone(tz);
289            return calDate;
290        }
291    
292        /**
293         * Create a Calendar instance for UTC time zone using the specified date.
294         * @param dateString
295         * @return appropriate Calendar object
296         * @throws Exception
297         */
298        public static Calendar getCalendar(String dateString) throws Exception {
299            return getCalendar(dateString, ACTIVE_TIMEZONE);
300        }
301    
302        /**
303         * Convert java.sql.Timestamp to java.util.Date
304         *
305         * @param timestamp java.sql.Timestamp
306         * @return java.util.Date
307         */
308        public static java.util.Date toDate(java.sql.Timestamp timestamp) {
309            if (timestamp != null) {
310                long milliseconds = timestamp.getTime();
311                return new java.util.Date(milliseconds);
312            }
313            return null;
314        }
315    
316        /**
317         * Convert java.util.Date to java.sql.Timestamp
318         *
319         * @param d java.util.Date
320         * @return java.sql.Timestamp
321         */
322        public static Timestamp convertDateToTimestamp(Date d) {
323            if (d != null) {
324                return new Timestamp(d.getTime());
325            }
326            return null;
327        }
328    
329    }