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