blob: 99ad2331dc68d893703523ea4f0511333cb7b837 [file] [log] [blame]
/**
* 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.lens.cube.metadata;
import static java.util.Calendar.MONTH;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.apache.lens.cube.error.LensCubeErrorCode;
import org.apache.lens.server.api.error.LensException;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.time.DateUtils;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public final class DateUtil {
private DateUtil() {
}
/*
* NOW -> new java.util.Date() NOW-7DAY -> a date one week earlier NOW (+-)
* <NUM>UNIT or Hardcoded dates in DD-MM-YYYY hh:mm:ss,sss
*/
public static final String UNIT;
public static final Date MAX_DATE = new Date(Long.MAX_VALUE);
public static final Date MIN_DATE = new Date(Long.MIN_VALUE);
static {
StringBuilder sb = new StringBuilder();
String sep = "";
for (UpdatePeriod up : UpdatePeriod.values()) {
sb.append(sep).append(up.getUnitName());
sep = "|";
}
UNIT = sb.toString();
}
public static final String GRANULARITY = "\\.(" + UNIT + ")";
public static final String RELATIVE = "(now)(" + GRANULARITY + ")?";
public static final Pattern P_RELATIVE = Pattern.compile(RELATIVE, Pattern.CASE_INSENSITIVE);
public static final String WSPACE = "\\s+";
public static final String OPTIONAL_WSPACE = "\\s*";
public static final String SIGNAGE = "\\+|\\-";
public static final Pattern P_SIGNAGE = Pattern.compile(SIGNAGE);
public static final String QUANTITY = "\\d+";
public static final Pattern P_QUANTITY = Pattern.compile(QUANTITY);
public static final Pattern P_UNIT = Pattern.compile(UNIT, Pattern.CASE_INSENSITIVE);
public static final String RELDATE_VALIDATOR_STR = RELATIVE + OPTIONAL_WSPACE + "((" + SIGNAGE + ")" + "("
+ WSPACE + ")?" + "(" + QUANTITY + ")" + OPTIONAL_WSPACE + "(" + UNIT + "))?" + "(s?)";
public static final Pattern RELDATE_VALIDATOR = Pattern.compile(RELDATE_VALIDATOR_STR, Pattern.CASE_INSENSITIVE);
public static final Pattern TIMESTAMP_VALIDATOR = Pattern.compile("\\d{5,}");
public static final String YEAR_FMT = "[0-9]{4}";
public static final String MONTH_FMT = YEAR_FMT + "-[0-9]{2}";
public static final String DAY_FMT = MONTH_FMT + "-[0-9]{2}";
public static final String HOUR_FMT = DAY_FMT + "-[0-9]{2}";
public static final String MINUTE_FMT = HOUR_FMT + ":[0-9]{2}";
public static final String SECOND_FMT = MINUTE_FMT + ":[0-9]{2}";
public static final String MILLISECOND_FMT = SECOND_FMT + ",[0-9]{3}";
public static final String ABSDATE_FMT = "yyyy-MM-dd-HH:mm:ss,SSS";
public static final String HIVE_QUERY_DATE_FMT = "yyyy-MM-dd HH:mm:ss";
public static final ThreadLocal<DateFormat> ABSDATE_PARSER =
new ThreadLocal<DateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat(ABSDATE_FMT);
}
};
public static final ThreadLocal<DateFormat> HIVE_QUERY_DATE_PARSER =
new ThreadLocal<DateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat(HIVE_QUERY_DATE_FMT);
}
};
public static String getAbsDateFormatString(String str) {
if (str.matches(YEAR_FMT)) {
return str + "-01-01-00:00:00,000";
} else if (str.matches(MONTH_FMT)) {
return str + "-01-00:00:00,000";
} else if (str.matches(DAY_FMT)) {
return str + "-00:00:00,000";
} else if (str.matches(HOUR_FMT)) {
return str + ":00:00,000";
} else if (str.matches(MINUTE_FMT)) {
return str + ":00,000";
} else if (str.matches(SECOND_FMT)) {
return str + ",000";
} else if (str.matches(MILLISECOND_FMT)) {
return str;
}
throw new IllegalArgumentException("Unsupported formatting for date" + str);
}
public static Date resolveDate(String str, Date now) throws LensException {
if (TIMESTAMP_VALIDATOR.matcher(str).matches()) {
return new Date(Long.parseLong(str));
} else if (RELDATE_VALIDATOR.matcher(str).matches()) {
return resolveRelativeDate(str, now);
} else {
return resolveAbsoluteDate(str);
}
}
public static String relativeToAbsolute(String relative) throws LensException {
return relativeToAbsolute(relative, new Date());
}
public static String relativeToAbsolute(String relative, Date now) throws LensException {
if (RELDATE_VALIDATOR.matcher(relative).matches()) {
return ABSDATE_PARSER.get().format(resolveRelativeDate(relative, now));
} else {
return relative;
}
}
static Cache<String, Date> stringToDateCache = CacheBuilder.newBuilder()
.expireAfterWrite(2, TimeUnit.HOURS).maximumSize(100).build();
public static Date resolveAbsoluteDate(final String str) throws LensException {
try {
return stringToDateCache.get(str, new Callable<Date>() {
@Override
public Date call() throws ParseException {
return ABSDATE_PARSER.get().parse(getAbsDateFormatString(str));
}
});
} catch (Exception e) {
log.error("Invalid date format. expected only {} date provided:{}", ABSDATE_FMT, str, e);
throw new LensException(LensCubeErrorCode.WRONG_TIME_RANGE_FORMAT.getLensErrorInfo(), ABSDATE_FMT, str);
}
}
public static Date resolveRelativeDate(String str, Date now) throws LensException {
if (StringUtils.isBlank(str)) {
throw new LensException(LensCubeErrorCode.NULL_DATE_VALUE.getLensErrorInfo());
}
// Resolve NOW with proper granularity
Calendar calendar = Calendar.getInstance();
calendar.setTime(now);
str = str.toLowerCase();
Matcher relativeMatcher = P_RELATIVE.matcher(str);
if (relativeMatcher.find()) {
String nowWithGranularity = relativeMatcher.group();
nowWithGranularity = nowWithGranularity.replaceAll("now", "");
nowWithGranularity = nowWithGranularity.replaceAll("\\.", "");
Matcher granularityMatcher = P_UNIT.matcher(nowWithGranularity);
if (granularityMatcher.find()) {
calendar = UpdatePeriod.fromUnitName(granularityMatcher.group().toLowerCase()).truncate(calendar);
}
}
// Get rid of 'now' part and whitespace
String diffStr = str.replaceAll(RELATIVE, "").replace(WSPACE, "");
TimeDiff diff = TimeDiff.parseFrom(diffStr);
return diff.offsetFrom(calendar.getTime());
}
public static Date getCeilDate(Date date, UpdatePeriod interval) {
return interval.getCeilDate(date);
}
public static Date getFloorDate(Date date, UpdatePeriod interval) {
return interval.getFloorDate(date);
}
public static CoveringInfo getMonthlyCoveringInfo(Date from, Date to) {
// Move 'from' to end of month, unless its the first day of month
boolean coverable = true;
if (!from.equals(DateUtils.truncate(from, MONTH))) {
from = DateUtils.addMonths(DateUtils.truncate(from, MONTH), 1);
coverable = false;
}
// Move 'to' to beginning of next month, unless its the first day of the month
if (!to.equals(DateUtils.truncate(to, MONTH))) {
to = DateUtils.truncate(to, MONTH);
coverable = false;
}
int months = 0;
while (from.before(to)) {
from = DateUtils.addMonths(from, 1);
months++;
}
return new CoveringInfo(months, coverable);
}
public static CoveringInfo getQuarterlyCoveringInfo(Date from, Date to) {
CoveringInfo monthlyCoveringInfo = getMonthlyCoveringInfo(from, to);
if (monthlyCoveringInfo.getCountBetween() < 3) {
return new CoveringInfo(0, false);
}
boolean coverable = monthlyCoveringInfo.isCoverable();
if (!from.equals(DateUtils.truncate(from, MONTH))) {
from = DateUtils.addMonths(DateUtils.truncate(from, MONTH), 1);
coverable = false;
}
Calendar cal = Calendar.getInstance();
cal.setTime(from);
int fromMonth = cal.get(MONTH);
// Get the start date of the quarter
int beginOffset = (3 - fromMonth % 3) % 3;
int endOffset = (monthlyCoveringInfo.getCountBetween() - beginOffset) % 3;
if (beginOffset > 0 || endOffset > 0) {
coverable = false;
}
return new CoveringInfo((monthlyCoveringInfo.getCountBetween() - beginOffset - endOffset) / 3, coverable);
}
public static CoveringInfo getYearlyCoveringInfo(Date from, Date to) {
CoveringInfo monthlyCoveringInfo = getMonthlyCoveringInfo(from, to);
if (monthlyCoveringInfo.getCountBetween() < 12) {
return new CoveringInfo(0, false);
}
boolean coverable = monthlyCoveringInfo.isCoverable();
if (!from.equals(DateUtils.truncate(from, MONTH))) {
from = DateUtils.addMonths(DateUtils.truncate(from, MONTH), 1);
coverable = false;
}
Calendar cal = Calendar.getInstance();
cal.setTime(from);
int fromMonth = cal.get(MONTH);
int beginOffset = (12 - fromMonth % 12) % 12;
int endOffset = (monthlyCoveringInfo.getCountBetween() - beginOffset) % 12;
if (beginOffset > 0 || endOffset > 0) {
coverable = false;
}
return new CoveringInfo((monthlyCoveringInfo.getCountBetween() - beginOffset - endOffset) / 12, coverable);
}
public static CoveringInfo getWeeklyCoveringInfo(Date from, Date to) {
int dayDiff = 0;
Date tmpFrom = from;
while (tmpFrom.before(to)) {
tmpFrom = DateUtils.addDays(tmpFrom, 1);
dayDiff++;
}
if (dayDiff < 7) {
return new CoveringInfo(0, false);
}
Calendar cal = Calendar.getInstance();
cal.setTime(from);
int fromDay = cal.get(Calendar.DAY_OF_WEEK);
cal.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
Date fromWeekStartDate = cal.getTime();
boolean coverable = dayDiff % 7 == 0;
if (fromWeekStartDate.before(from)) {
// Count from the start of next week
dayDiff -= (cal.getActualMaximum(Calendar.DAY_OF_WEEK) - (fromDay - Calendar.SUNDAY));
coverable = false;
}
return new CoveringInfo(dayDiff / 7, coverable);
}
static CoveringInfo getCoveringInfo(Date from, Date to, UpdatePeriod interval) {
switch (interval) {
case SECONDLY:
case CONTINUOUS:
return getMilliSecondCoveringInfo(from, to, 1000, interval);
case MINUTELY:
case HOURLY:
case DAILY:
return getMilliSecondCoveringInfo(from, to, interval.weight(), interval);
case WEEKLY:
return getWeeklyCoveringInfo(from, to);
case MONTHLY:
return getMonthlyCoveringInfo(from, to);
case QUARTERLY:
return getQuarterlyCoveringInfo(from, to);
case YEARLY:
return getYearlyCoveringInfo(from, to);
default:
return new CoveringInfo(0, false);
}
}
private static CoveringInfo getMilliSecondCoveringInfo(Date from, Date to, long millisInInterval,
UpdatePeriod interval) {
long diff = to.getTime() - from.getTime();
return new CoveringInfo((int) (diff / millisInInterval),
Stream.of(from, to).allMatch(a->interval.truncate(a).equals(a)));
// start date and end date should lie on boundaries.
}
/**
* Whether the range [from,to) is coverable by intervals
* @param from from time
* @param to to time
* @param intervals intervals to check
* @return true if any of the intervals can completely cover the range
*/
static boolean isCoverableBy(Date from, Date to, Set<UpdatePeriod> intervals) {
return intervals.stream().anyMatch(period->isCoverableBy(from, to, period));
}
private static boolean isCoverableBy(Date from, Date to, UpdatePeriod period) {
return getCoveringInfo(from, to, period).isCoverable();
}
public static int getTimeDiff(Date fromDate, Date toDate, UpdatePeriod updatePeriod) {
if (fromDate.before(toDate)) {
return getCoveringInfo(fromDate, toDate, updatePeriod).getCountBetween();
} else {
return -getCoveringInfo(toDate, fromDate, updatePeriod).getCountBetween();
}
}
@Data
public static class CoveringInfo {
int countBetween;
boolean coverable;
public CoveringInfo(int countBetween, boolean coverable) {
this.countBetween = countBetween;
this.coverable = coverable;
}
}
@EqualsAndHashCode
public static class TimeDiff {
int quantity;
UpdatePeriod updatePeriod;
private TimeDiff(int quantity, UpdatePeriod updatePeriod) {
this.quantity = quantity;
this.updatePeriod = updatePeriod;
}
public static TimeDiff parseFrom(String diffStr) throws LensException {
// Get the relative diff part to get eventual date based on now.
Matcher qtyMatcher = P_QUANTITY.matcher(diffStr);
int qty = 1;
if (qtyMatcher.find()) {
qty = Integer.parseInt(qtyMatcher.group());
}
Matcher signageMatcher = P_SIGNAGE.matcher(diffStr);
if (signageMatcher.find()) {
String sign = signageMatcher.group();
if ("-".equals(sign)) {
qty = -qty;
}
}
Matcher unitMatcher = P_UNIT.matcher(diffStr);
if (unitMatcher.find()) {
return new TimeDiff(qty, UpdatePeriod.fromUnitName(unitMatcher.group().toLowerCase()));
}
return new TimeDiff(0, UpdatePeriod.CONTINUOUS);
}
public Date offsetFrom(Date time) {
return DateUtils.add(time, updatePeriod.calendarField(), quantity);
}
public Date negativeOffsetFrom(Date time) {
return DateUtils.add(time, updatePeriod.calendarField(), -quantity);
}
}
}