TimeUtil.java

package emissary.util;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAccessor;
import java.time.zone.ZoneRulesException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;

public class TimeUtil {

    private static final ZoneId GMT = ZoneId.of("GMT");
    public static final DateTimeFormatter DATE_WITH_SLASHES = DateTimeFormatter.ofPattern("yyyy-MM-dd/HH/mm").withZone(GMT);
    public static final DateTimeFormatter DATE_ISO_8601 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(GMT);
    public static final DateTimeFormatter DATE_FULL_ISO_8601 = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssX").withZone(GMT);
    public static final DateTimeFormatter DATE_ORDINAL = DateTimeFormatter.ofPattern("yyyyDDD").withZone(GMT);
    public static final DateTimeFormatter DATE_ISO_8601_SSS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
    public static final DateTimeFormatter DATE_ORDINAL_WITH_TIME = DateTimeFormatter.ofPattern("yyyyDDDHHmmss");
    private static final String ISO_8601_TIME_DATE_STRING = "yyyy-MM-dd'T'HH:mm:ss'Z'";
    private static final String HEX_REGEX = "^0x([0-9A-Fa-f]{8})([0-9A-Fa-f]{8})";
    private static final Pattern HEX_DATE_PATTERN = Pattern.compile(HEX_REGEX);
    private static final int HEX_RADIX = 16;
    private static final LocalDateTime STARTING_DATE = LocalDateTime.parse("1900-01-01 00:00:00.000", DATE_ISO_8601_SSS);
    private static final Logger logger = LoggerFactory.getLogger(TimeUtil.class);

    /**
     * Get the application configured timezone
     *
     * @return the timezone
     */
    public static ZoneId getTimezone() {
        return GMT;
    }

    /**
     * Get current date/time formatted as specified
     *
     * @param format the specification
     * @param timeZone tz to use, defaults to GMT
     * @throws DateTimeException if the ID format is invalid
     * @throws ZoneRulesException if checking availability and the zone ID cannot be found
     * @throws IllegalArgumentException if the pattern is invalid
     */
    public static String getDate(final String format, @Nullable final String timeZone) {
        ZoneId zoneId = timeZone == null ? GMT : ZoneId.of(timeZone);
        return getDate(ZonedDateTime.now(zoneId), format, zoneId);
    }

    /**
     * Given a date, format it with the given pattern
     *
     * @param date the date to format
     * @param format the date format
     * @param timeZone the time zone id
     * @return the formatted date
     * @throws DateTimeException if the ID format is invalid
     * @throws ZoneRulesException if checking availability and the zone ID cannot be found
     * @throws IllegalArgumentException if the pattern is invalid
     */
    @Nullable
    public static String getDate(@Nullable final TemporalAccessor date, final String format, @Nullable final ZoneId timeZone) {
        if (date == null) {
            return null;
        }

        if (StringUtils.isEmpty(format)) {
            throw new IllegalArgumentException("The date format cannot be empty!");
        }

        ZoneId zoneId = timeZone == null ? GMT : timeZone;
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format).withZone(zoneId);
        return formatter.format(date);
    }

    /**
     * Get current date/time as a slash separated path component (yyyy-MM-dd/HH/mm)
     *
     * @return the formatted date in the form yyyy-MM-dd/HH/mm
     * @throws DateTimeException if an error occurs during formatting
     */
    public static String getCurrentDate() {
        return getDateAsPath(ZonedDateTime.now(GMT));
    }

    /**
     * Get specified date/time as a slash separated path component (yyyy-MM-dd/HH/mm)
     *
     * @param date to format
     * @return the formatted date in the form yyyy-MM-dd/HH/mm
     * @throws DateTimeException if an error occurs during formatting
     */
    @Nullable
    public static String getDateAsPath(@Nullable final TemporalAccessor date) {
        if (date == null) {
            return null;
        }
        String dateStringDefault = DATE_WITH_SLASHES.format(date);
        return dateStringDefault.substring(0, dateStringDefault.length() - 1) + "0";
    }

    /**
     * Get current date as yyyyjjj
     *
     * @return the formatted date in the form yyyyjjj
     * @throws DateTimeException if an error occurs during formatting
     */
    public static String getCurrentDateOrdinal() {
        return getDateOrdinal(ZonedDateTime.now(GMT));
    }

    /**
     * Get current date as yyyyjjj
     *
     * @param date to format
     * @return the formatted date in the form yyyyjjj
     * @throws DateTimeException if an error occurs during formatting
     */
    @Nullable
    public static String getDateOrdinal(@Nullable TemporalAccessor date) {
        return date == null ? null : DATE_ORDINAL.format(date);
    }

    /**
     * Get current date/time as ISO-8601 string
     *
     * @return String in the format yyyy-MM-dd HH:mm:ss
     * @throws DateTimeException if an error occurs during formatting
     */
    @SuppressWarnings("MemberName")
    public static String getCurrentDateISO8601() {
        return getDateAsISO8601(ZonedDateTime.now(GMT));
    }

    /**
     * Get specified time as ISO-8601 string
     *
     * @return String in the format yyyy-MM-dd HH:mm:ss
     * @throws DateTimeException if an error occurs during formatting
     */
    @SuppressWarnings("MemberName")
    public static String getDateAsISO8601(final long time) {
        return getDateAsISO8601(ZonedDateTime.ofInstant(Instant.ofEpochMilli(time), GMT));
    }

    /**
     * Get specified date object as ISO-8601 string (yyyy-MM-dd HH:mm:ss)
     *
     * @param date to format
     * @return String in the format yyyy-MM-dd HH:mm:ss
     * @throws DateTimeException if an error occurs during formatting
     */
    @Nullable
    @SuppressWarnings("MemberName")
    public static String getDateAsISO8601(@Nullable final TemporalAccessor date) {
        return date == null ? null : DATE_ISO_8601.format(date);
    }

    /**
     * Get current date/time as full ISO-8601 string yyyy-MM-dd'T'HH:mm:ss'Z'
     *
     * @return String in the format yyyy-MM-dd'T'HH:mm:ss'Z'
     * @throws DateTimeException if an error occurs during formatting
     */
    @SuppressWarnings("MemberName")
    public static String getCurrentDateFullISO8601() {
        return getDateAsFullISO8601(ZonedDateTime.now(GMT));
    }

    /**
     * Get specified date object as a full ISO-8601 string (yyyy-MM-dd'T'HH:mm:ss'Z')
     *
     * @param date to format
     * @return the formatted date in yyyy-MM-dd'T'HH:mm:ss'Z'
     * @throws DateTimeException if an error occurs during formatting
     */
    @Nullable
    @SuppressWarnings("MemberName")
    public static String getDateAsFullISO8601(@Nullable final TemporalAccessor date) {
        return date == null ? null : DATE_FULL_ISO_8601.format(date);
    }

    /**
     * Get Date object from ISO-8601 formatted string ("yyyy-MM-dd HH:mm:ss")
     *
     * @param dateString the string representation of the date in the format yyyy-MM-dd HH:mm:ss
     * @return ZonedDateTime parsed from the string
     * @throws DateTimeParseException if string is not in the proper format
     *
     * @deprecated replaced by {@link FlexibleDateTimeParser#parse(String)}
     */
    @Nullable
    @Deprecated
    @SuppressWarnings("MemberName")
    public static ZonedDateTime getZonedDateFromISO8601(@Nullable final String dateString) {
        return dateString == null ? null : ZonedDateTime.parse(dateString, DATE_ISO_8601);
    }

    /**
     * Parses hex date string into formatted string ("yyyy-MM-dd HH:mm:ss.SSS")
     *
     * @param hexDate hex string representation of the date
     * @return converted time in string format yyyy-MM-dd HH:mm:ss.SSS
     * @throws DateTimeParseException if could not parse date
     *
     */
    public static String convertHexDate(String hexDate) {
        Matcher m = HEX_DATE_PATTERN.matcher(hexDate);

        if (m.find()) {
            String dateHex = m.group(1);
            String timeHex = m.group(2);
            long daysToAdd = Long.parseLong(dateHex, HEX_RADIX);
            // timeHex represents the number of ticks (1/300 of a second) since midnight
            long millisToAdd = Math.round(Long.parseLong(timeHex, HEX_RADIX) * 10 / 3.0);

            LocalDateTime ldt;

            try {
                ldt = STARTING_DATE.plusDays(daysToAdd);
                ldt = ldt.plus(millisToAdd, ChronoUnit.MILLIS);
            } catch (DateTimeParseException ex) {
                logger.debug("Could not parse date", ex);
                throw ex;
            }

            return ldt.format(DATE_ISO_8601_SSS);
        }

        String msg = String.format("Unexpected hexDate format '%s'", hexDate);
        logger.debug(msg);
        throw new IllegalArgumentException(msg);
    }

    @SuppressWarnings("MemberName")
    public static String getISO8601DateFormatString() {
        return ISO_8601_TIME_DATE_STRING;
    }

    public static String getDateOrdinalWithTime(final Instant d) {
        return DATE_ORDINAL_WITH_TIME.format(d.atZone(ZoneId.systemDefault()));
    }

    /** This class is not meant to be instantiated. */
    private TimeUtil() {}
}