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 }