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}