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 */
018package org.apache.oozie.util;
019
020import java.sql.Timestamp;
021import java.text.DateFormat;
022import java.text.ParseException;
023import java.text.ParsePosition;
024import java.text.SimpleDateFormat;
025import java.util.Calendar;
026import java.util.Date;
027import java.util.GregorianCalendar;
028import java.util.TimeZone;
029import java.util.regex.Matcher;
030import java.util.regex.Pattern;
031
032import org.apache.hadoop.conf.Configuration;
033import org.apache.oozie.coord.TimeUnit;
034
035/**
036 * Date utility classes to parse and format datetimes in Oozie expected datetime formats.
037 */
038public 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}