/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.falcon.util; import org.apache.commons.lang3.StringUtils; import org.apache.falcon.FalconException; import org.apache.falcon.entity.v0.SchemaHelper; import org.apache.falcon.entity.v0.Frequency; import java.text.DateFormat; import java.text.ParseException; import java.text.ParsePosition; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Helper to get date operations. */ public final class DateUtil { private static final long MINUTE_IN_MS = 60 * 1000L; private static final long HOUR_IN_MS = 60 * MINUTE_IN_MS; private static final long DAY_IN_MS = 24 * HOUR_IN_MS; private static final long MONTH_IN_MS = 31 * DAY_IN_MS; //Friday, April 16, 9999 7:12:55 AM UTC corresponding date public static final Date NEVER = new Date(Long.parseLong("253379862775000")); public static final long HOUR_IN_MILLIS = 60 * 60 * 1000; private static final Pattern GMT_OFFSET_COLON_PATTERN = Pattern.compile("^GMT(\\-|\\+)(\\d{2})(\\d{2})$"); public static final TimeZone UTC = getTimeZone("UTC"); public static final String ISO8601_UTC_MASK = "yyyy-MM-dd'T'HH:mm'Z'"; private static String activeTimeMask = ISO8601_UTC_MASK; private static TimeZone activeTimeZone = UTC; private static final Pattern VALID_TIMEZONE_PATTERN = Pattern.compile("^UTC$|^GMT(\\+|\\-)\\d{4}$"); private static final String ISO8601_TZ_MASK_WITHOUT_OFFSET = "yyyy-MM-dd'T'HH:mm"; private static boolean entityInUTC = true; private DateUtil() {} /** * Configures the Datetime parsing with process timezone. * */ public static void setTimeZone(String tz) throws FalconException { if (StringUtils.isBlank(tz)) { tz = "UTC"; } if (!VALID_TIMEZONE_PATTERN.matcher(tz).matches()) { throw new FalconException("Invalid entity timezone, it must be 'UTC' or 'GMT(+/-)####"); } activeTimeZone = TimeZone.getTimeZone(tz); entityInUTC = activeTimeZone.equals(UTC); activeTimeMask = (entityInUTC) ? ISO8601_UTC_MASK : ISO8601_TZ_MASK_WITHOUT_OFFSET + tz.substring(3); } public static Date getNextMinute(Date time) { Calendar insCal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); insCal.setTime(time); insCal.add(Calendar.MINUTE, 1); return insCal.getTime(); } public static String getDateFormatFromTime(long milliSeconds) { return SchemaHelper.getDateFormat().format((new Date(milliSeconds))); } /** * This function should not be used for scheduling related functions as it may cause correctness issues in those * scenarios. * @param frequency * @return */ public static Long getFrequencyInMillis(Frequency frequency){ switch (frequency.getTimeUnit()) { case months: return MONTH_IN_MS * frequency.getFrequencyAsInt(); case days: return DAY_IN_MS * frequency.getFrequencyAsInt(); case hours: return HOUR_IN_MS * frequency.getFrequencyAsInt(); case minutes: return MINUTE_IN_MS * frequency.getFrequencyAsInt(); default: return null; } } /** * Returns the current time, with seconds and milliseconds reset to 0. * @return */ public static Date now() { Calendar cal = Calendar.getInstance(); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0); return cal.getTime(); } /** * Adds the supplied number of seconds to the given date and returns the new Date. * @param date * @param seconds * @return */ public static Date offsetTime(Date date, int seconds) { return new Date(1000L * seconds + date.getTime()); } /** * Parses a datetime in ISO8601 format in the process timezone. * * @param s string with the datetime to parse. * @return the corresponding {@link java.util.Date} instance for the parsed date. * @throws java.text.ParseException thrown if the given string was * not an ISO8601 value for the process timezone. */ public static Date parseDateFalconTZ(String s) throws ParseException { s = s.trim(); ParsePosition pos = new ParsePosition(0); Date d = getISO8601DateFormat(activeTimeZone, activeTimeMask).parse(s, pos); if (d == null) { throw new ParseException("Could not parse [" + s + "] using [" + activeTimeMask + "] mask", pos.getErrorIndex()); } if (s.length() > pos.getIndex()) { throw new ParseException("Correct datetime string is followed by invalid characters: " + s, pos.getIndex()); } return d; } private static DateFormat getISO8601DateFormat(TimeZone tz, String mask) { DateFormat dateFormat = new SimpleDateFormat(mask); // Stricter parsing to prevent dates such as 2011-12-50T01:00Z (December 50th) from matching dateFormat.setLenient(false); dateFormat.setTimeZone(tz); return dateFormat; } private static DateFormat getSpecificDateFormat(String format) { DateFormat dateFormat = new SimpleDateFormat(format); dateFormat.setTimeZone(activeTimeZone); return dateFormat; } /** * Formats a {@link java.util.Date} as a string using the specified format mask. * <p/> * The format mask must be a {@link java.text.SimpleDateFormat} valid format mask. * * @param d {@link java.util.Date} to format. * @return the string for the given date using the specified format mask, * <code>NULL</code> if the {@link java.util.Date} instance was <code>NULL</code> */ public static String formatDateCustom(Date d, String format) { return (d != null) ? getSpecificDateFormat(format).format(d) : "NULL"; } /** * Formats a {@link java.util.Date} as a string in ISO8601 format using process timezone. * * @param d {@link java.util.Date} to format. * @return the ISO8601 string for the given date, <code>NULL</code> if the {@link java.util.Date} instance was * <code>NULL</code> */ public static String formatDateFalconTZ(Date d) { return (d != null) ? getISO8601DateFormat(activeTimeZone, activeTimeMask).format(d) : "NULL"; } /** * Returns the {@link java.util.TimeZone} for the given timezone ID. * * @param tzId timezone ID. * @return the {@link java.util.TimeZone} for the given timezone ID. */ public static TimeZone getTimeZone(String tzId) { if (tzId == null) { throw new IllegalArgumentException("Timezone cannot be null"); } tzId = handleGMTOffsetTZNames(tzId); // account for GMT-#### TimeZone tz = TimeZone.getTimeZone(tzId); // If these are not equal, it means that the tzId is not valid (invalid tzId's return GMT) if (!tz.getID().equals(tzId)) { throw new IllegalArgumentException("Invalid TimeZone: " + tzId); } return tz; } /** * {@link java.util.TimeZone#getTimeZone(String)} takes the timezone ID as an argument; for invalid IDs * it returns the <code>GMT</code> TimeZone. A timezone ID formatted like <code>GMT-####</code> is not a valid ID, * however, it will actually map this to the <code>GMT-##:##</code> TimeZone, instead of returning the * <code>GMT</code> TimeZone. We check (later) check that a timezone ID is valid by calling * {@link java.util.TimeZone#getTimeZone(String)} and seeing if the returned * TimeZone ID is equal to the original; because we want to allow <code>GMT-####</code>, while still * disallowing actual invalid IDs, we have to manually replace <code>GMT-####</code> * with <code>GMT-##:##</code> first. * * @param tzId The timezone ID * @return If tzId matches <code>GMT-####</code>, then we return <code>GMT-##:##</code>; otherwise, * we return tzId unaltered */ private static String handleGMTOffsetTZNames(String tzId) { Matcher m = GMT_OFFSET_COLON_PATTERN.matcher(tzId); if (m.matches() && m.groupCount() == 3) { tzId = "GMT" + m.group(1) + m.group(2) + ":" + m.group(3); } return tzId; } /** * Create a Calendar instance for UTC time zone using the specified date. * @param dateString * @return appropriate Calendar object * @throws Exception */ public static Calendar getCalendar(String dateString) throws Exception { return getCalendar(dateString, activeTimeZone); } /** * Create a Calendar instance using the specified date and Time zone. * @param dateString * @param tz : TimeZone * @return appropriate Calendar object * @throws Exception */ public static Calendar getCalendar(String dateString, TimeZone tz) throws Exception { Date date = DateUtil.parseDateFalconTZ(dateString); Calendar calDate = Calendar.getInstance(); calDate.setTime(date); calDate.setTimeZone(tz); return calDate; } /** * Formats a {@link java.util.Calendar} as a string in ISO8601 format process timezone. * * @param c {@link java.util.Calendar} to format. * @return the ISO8601 string for the given date, <code>NULL</code> if the {@link java.util.Calendar} instance was * <code>NULL</code> */ public static String formatDateFalconTZ(Calendar c) { return (c != null) ? formatDateFalconTZ(c.getTime()) : "NULL"; } }