| // | |
| // 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 com.cloud.utils; | |
| import java.text.ParseException; | |
| import java.time.ZonedDateTime; | |
| import java.time.YearMonth; | |
| import java.util.Calendar; | |
| import java.util.Date; | |
| import java.util.TimeZone; | |
| import java.util.concurrent.ConcurrentHashMap; | |
| import java.time.LocalDateTime; | |
| import java.time.ZoneId; | |
| import java.time.format.DateTimeFormatter; | |
| import java.time.format.DateTimeParseException; | |
| import java.time.OffsetDateTime; | |
| import com.cloud.utils.exception.CloudRuntimeException; | |
| import com.cronutils.descriptor.CronDescriptor; | |
| import com.cronutils.model.CronType; | |
| import com.cronutils.model.definition.CronDefinition; | |
| import com.cronutils.model.definition.CronDefinitionBuilder; | |
| import com.cronutils.parser.CronParser; | |
| import org.springframework.scheduling.support.CronExpression; | |
| public class DateUtil { | |
| public static final int HOURS_IN_A_MONTH = 30 * 24; | |
| public static final TimeZone GMT_TIMEZONE = TimeZone.getTimeZone("GMT"); | |
| public static final String YYYYMMDD_FORMAT = "yyyyMMddHHmmss"; | |
| public static final String ZONED_DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; | |
| private static final DateTimeFormatter OUTPUT_FORMATTER = | |
| DateTimeFormatter.ofPattern(ZONED_DATETIME_FORMAT).withZone(ZoneId.systemDefault()); | |
| private static final ConcurrentHashMap<String, DateTimeFormatter> s_formatterCache = new ConcurrentHashMap<>(); | |
| private static final DateTimeFormatter[] parseFormats = new DateTimeFormatter[]{ | |
| DateTimeFormatter.ISO_OFFSET_DATE_TIME, | |
| DateTimeFormatter.ofPattern(ZONED_DATETIME_FORMAT), | |
| DateTimeFormatter.ISO_INSTANT, | |
| // with milliseconds | |
| DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSX"), | |
| DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ"), | |
| DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"), | |
| // legacy and non-sensical format | |
| DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'Z") | |
| }; | |
| public static Date currentGMTTime() { | |
| // Date object always stores milliseconds offset based on GMT internally | |
| return new Date(); | |
| } | |
| private static DateTimeFormatter getFormatter(String pattern, ZoneId zone) { | |
| String key = pattern + "|" + zone.getId(); | |
| return s_formatterCache.computeIfAbsent(key, k -> DateTimeFormatter.ofPattern(pattern).withZone(zone)); | |
| } | |
| public static Date parseTZDateString(String str) throws ParseException { | |
| for (DateTimeFormatter formatter : parseFormats) { | |
| try { | |
| OffsetDateTime dt = OffsetDateTime.parse(str, formatter); | |
| return Date.from(dt.toInstant()); | |
| } catch (DateTimeParseException e) { | |
| // do nothing | |
| } | |
| } | |
| throw new ParseException("Unparseable date: \"" + str + "\"", 0); | |
| } | |
| public static Date parseDateString(TimeZone tz, String dateString) { | |
| return parseDateString(tz, dateString, "yyyy-MM-dd HH:mm:ss"); | |
| } | |
| public static Date parseDateString(TimeZone tz, String dateString, String formatString) { | |
| ZoneId zoneId = tz.toZoneId(); | |
| DateTimeFormatter formatter = getFormatter(formatString, zoneId); | |
| try { | |
| return Date.from(LocalDateTime.parse(dateString, formatter).atZone(zoneId).toInstant()); | |
| } catch (DateTimeParseException e) { | |
| throw new CloudRuntimeException("Failed to parse date string: " + dateString, e); | |
| } | |
| } | |
| public static String displayDateInTimezone(TimeZone tz, Date time) { | |
| return getDateDisplayString(tz, time, ZONED_DATETIME_FORMAT); | |
| } | |
| public static String getDateDisplayString(TimeZone tz, Date time) { | |
| return getDateDisplayString(tz, time, "yyyy-MM-dd HH:mm:ss"); | |
| } | |
| public static String getDateDisplayString(TimeZone tz, Date time, String formatString) { | |
| if (time == null) { | |
| return null; | |
| } | |
| return getFormatter(formatString, tz.toZoneId()).format(time.toInstant()); | |
| } | |
| public static String getOutputString(Date date) { | |
| if (date == null) { | |
| return ""; | |
| } | |
| return OUTPUT_FORMATTER.format(date.toInstant()); | |
| } | |
| public static Date now() { | |
| return new Date(System.currentTimeMillis()); | |
| } | |
| public enum IntervalType { | |
| HOURLY, DAILY, WEEKLY, MONTHLY; | |
| boolean equals(String intervalType) { | |
| return super.toString().equalsIgnoreCase(intervalType); | |
| } | |
| public static IntervalType getIntervalType(String intervalTypeStr) { | |
| for (IntervalType intervalType : IntervalType.values()) { | |
| if (intervalType.equals(intervalTypeStr)) { | |
| return intervalType; | |
| } | |
| } | |
| return null; | |
| } | |
| } | |
| public static IntervalType getIntervalType(short type) { | |
| if (type < 0 || type >= IntervalType.values().length) { | |
| return null; | |
| } | |
| return IntervalType.values()[type]; | |
| } | |
| /** | |
| * Return next run time | |
| * @param type hourly/daily/weekly/monthly | |
| * @param schedule MM[:HH][:DD] format. DD is day of week for weekly and day of month for monthly | |
| * @param timezone The timezone in which the schedule string is specified | |
| * @param startDate if specified, returns next run time after the specified startDate | |
| * @return | |
| */ | |
| public static Date getNextRunTime(IntervalType type, String schedule, String timezone, Date startDate) { | |
| String[] scheduleParts = schedule.split(":"); //MM:HH:DAY | |
| final Calendar scheduleTime = Calendar.getInstance(); | |
| scheduleTime.setTimeZone(TimeZone.getTimeZone(timezone)); | |
| if (startDate == null) { | |
| startDate = new Date(); | |
| } | |
| scheduleTime.setTime(startDate); | |
| // Throw an ArrayIndexOutOfBoundsException if schedule is badly formatted. | |
| scheduleTime.setLenient(false); | |
| int minutes = 0; | |
| int hour = 0; | |
| int day = 0; | |
| Date execDate; | |
| Date now = new Date(); | |
| switch (type) { | |
| case HOURLY: | |
| if (scheduleParts.length < 1) { | |
| throw new CloudRuntimeException("Incorrect schedule format: " + schedule + " for interval type:" + type.toString()); | |
| } | |
| minutes = Integer.parseInt(scheduleParts[0]); | |
| scheduleTime.set(Calendar.MINUTE, minutes); | |
| scheduleTime.set(Calendar.SECOND, 0); | |
| scheduleTime.set(Calendar.MILLISECOND, 0); | |
| try { | |
| execDate = scheduleTime.getTime(); | |
| } catch (IllegalArgumentException ex) { | |
| scheduleTime.setLenient(true); | |
| execDate = scheduleTime.getTime(); | |
| scheduleTime.setLenient(false); | |
| } | |
| // XXX: !execDate.after(startDate) is strictly for testing. | |
| // During testing we use a test clock which runs much faster than the real clock | |
| // So startDate and execDate will always be ahead in the future | |
| // and we will never increase the time here | |
| if (execDate.before(now) || !execDate.after(startDate)) { | |
| scheduleTime.add(Calendar.HOUR_OF_DAY, 1); | |
| } | |
| break; | |
| case DAILY: | |
| if (scheduleParts.length < 2) { | |
| throw new CloudRuntimeException("Incorrect schedule format: " + schedule + " for interval type:" + type.toString()); | |
| } | |
| minutes = Integer.parseInt(scheduleParts[0]); | |
| hour = Integer.parseInt(scheduleParts[1]); | |
| scheduleTime.set(Calendar.HOUR_OF_DAY, hour); | |
| scheduleTime.set(Calendar.MINUTE, minutes); | |
| scheduleTime.set(Calendar.SECOND, 0); | |
| scheduleTime.set(Calendar.MILLISECOND, 0); | |
| try { | |
| execDate = scheduleTime.getTime(); | |
| } catch (IllegalArgumentException ex) { | |
| scheduleTime.setLenient(true); | |
| execDate = scheduleTime.getTime(); | |
| scheduleTime.setLenient(false); | |
| } | |
| // XXX: !execDate.after(startDate) is strictly for testing. | |
| // During testing we use a test clock which runs much faster than the real clock | |
| // So startDate and execDate will always be ahead in the future | |
| // and we will never increase the time here | |
| if (execDate.before(now) || !execDate.after(startDate)) { | |
| scheduleTime.add(Calendar.DAY_OF_YEAR, 1); | |
| } | |
| break; | |
| case WEEKLY: | |
| if (scheduleParts.length < 3) { | |
| throw new CloudRuntimeException("Incorrect schedule format: " + schedule + " for interval type:" + type.toString()); | |
| } | |
| minutes = Integer.parseInt(scheduleParts[0]); | |
| hour = Integer.parseInt(scheduleParts[1]); | |
| day = Integer.parseInt(scheduleParts[2]); | |
| scheduleTime.set(Calendar.DAY_OF_WEEK, day); | |
| scheduleTime.set(Calendar.HOUR_OF_DAY, hour); | |
| scheduleTime.set(Calendar.MINUTE, minutes); | |
| scheduleTime.set(Calendar.SECOND, 0); | |
| scheduleTime.set(Calendar.MILLISECOND, 0); | |
| try { | |
| execDate = scheduleTime.getTime(); | |
| } catch (IllegalArgumentException ex) { | |
| scheduleTime.setLenient(true); | |
| execDate = scheduleTime.getTime(); | |
| scheduleTime.setLenient(false); | |
| } | |
| // XXX: !execDate.after(startDate) is strictly for testing. | |
| // During testing we use a test clock which runs much faster than the real clock | |
| // So startDate and execDate will always be ahead in the future | |
| // and we will never increase the time here | |
| if (execDate.before(now) || !execDate.after(startDate)) { | |
| scheduleTime.add(Calendar.DAY_OF_WEEK, 7); | |
| } | |
| break; | |
| case MONTHLY: | |
| if (scheduleParts.length < 3) { | |
| throw new CloudRuntimeException("Incorrect schedule format: " + schedule + " for interval type:" + type.toString()); | |
| } | |
| minutes = Integer.parseInt(scheduleParts[0]); | |
| hour = Integer.parseInt(scheduleParts[1]); | |
| day = Integer.parseInt(scheduleParts[2]); | |
| if (day > 28) { | |
| throw new CloudRuntimeException("Day cannot be greater than 28 for monthly schedule"); | |
| } | |
| scheduleTime.set(Calendar.DAY_OF_MONTH, day); | |
| scheduleTime.set(Calendar.HOUR_OF_DAY, hour); | |
| scheduleTime.set(Calendar.MINUTE, minutes); | |
| scheduleTime.set(Calendar.SECOND, 0); | |
| scheduleTime.set(Calendar.MILLISECOND, 0); | |
| try { | |
| execDate = scheduleTime.getTime(); | |
| } catch (IllegalArgumentException ex) { | |
| scheduleTime.setLenient(true); | |
| execDate = scheduleTime.getTime(); | |
| scheduleTime.setLenient(false); | |
| } | |
| // XXX: !execDate.after(startDate) is strictly for testing. | |
| // During testing we use a test clock which runs much faster than the real clock | |
| // So startDate and execDate will always be ahead in the future | |
| // and we will never increase the time here | |
| if (execDate.before(now) || !execDate.after(startDate)) { | |
| scheduleTime.add(Calendar.MONTH, 1); | |
| } | |
| break; | |
| default: | |
| throw new CloudRuntimeException("Incorrect interval: " + type.toString()); | |
| } | |
| try { | |
| return scheduleTime.getTime(); | |
| } catch (IllegalArgumentException ex) { | |
| scheduleTime.setLenient(true); | |
| Date nextScheduledDate = scheduleTime.getTime(); | |
| scheduleTime.setLenient(false); | |
| return nextScheduledDate; | |
| } | |
| } | |
| public static long getTimeDifference(Date date1, Date date2){ | |
| return (date1.getTime() - date2.getTime()) / 1000; | |
| } | |
| public static CronExpression parseSchedule(String schedule) { | |
| if (schedule != null) { | |
| // CronExpression's granularity is in seconds. Prepending "0 " to change the granularity to minutes. | |
| return CronExpression.parse(String.format("0 %s", schedule)); | |
| } else { | |
| return null; | |
| } | |
| } | |
| public static String getHumanReadableSchedule(CronExpression schedule) { | |
| CronDefinition cronDefinition = CronDefinitionBuilder.instanceDefinitionFor(CronType.SPRING); | |
| CronParser parser = new CronParser(cronDefinition); | |
| CronDescriptor descriptor = CronDescriptor.instance(); | |
| return descriptor.describe(parser.parse(schedule.toString())); | |
| } | |
| public static ZonedDateTime getZoneDateTime(Date date, ZoneId tzId) { | |
| if (date == null) { | |
| return null; | |
| } | |
| ZonedDateTime zonedDate = ZonedDateTime.ofInstant(date.toInstant(), tzId); | |
| LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), TimeZone.getDefault().toZoneId()); | |
| zonedDate = zonedDate.withYear(localDateTime.getYear()) | |
| .withMonth(localDateTime.getMonthValue()) | |
| .withDayOfMonth(localDateTime.getDayOfMonth()) | |
| .withHour(localDateTime.getHour()) | |
| .withMinute(localDateTime.getMinute()) | |
| .withSecond(localDateTime.getSecond()); | |
| return zonedDate; | |
| } | |
| public static int getHoursInCurrentMonth(Date date) { | |
| return YearMonth.of(date.getYear(), date.getMonth() + 1).lengthOfMonth() * 24; | |
| } | |
| } |