001/*
002 * ModeShape (http://www.modeshape.org)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *       http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.modeshape.common.util;
017
018import java.time.Instant;
019import java.time.LocalDate;
020import java.time.LocalDateTime;
021import java.time.LocalTime;
022import java.time.ZoneId;
023import java.time.ZonedDateTime;
024import java.time.format.DateTimeFormatter;
025import java.time.format.DateTimeFormatterBuilder;
026import java.time.format.DateTimeParseException;
027import java.time.format.SignStyle;
028import java.time.temporal.ChronoField;
029import java.time.temporal.TemporalAccessor;
030
031/**
032 * Utility for interacting with various date and time objects.
033 * 
034 * @author Horia Chiorean (hchiorea@redhat.com)
035 */
036public final class DateTimeUtil {
037
038    /**
039     * UTC zone id
040     */
041    public static final ZoneId UTC = ZoneId.of("UTC");
042    
043    /**
044     * ISO 8601 formatter which attempts to be as close to the previous behavior (JODA) as possible. JDK 8 has some
045     * significant differences especially when it comes to milliseconds, which it doesn't support out-of-the-box. 
046     * 
047     * However, because of this bug in JDK 8: 
048     * 
049     * - https://bugs.openjdk.java.net/browse/JDK-8031085 this expression
050     * - http://bugs.java.com/view_bug.do?bug_id=8032491
051     * 
052     * is WAY MORE COMPLICATED than it should be (in reality is should use the .SSS pattern)
053     */
054    private static DateTimeFormatter JODA_ISO8601_FORMATTER = new DateTimeFormatterBuilder()
055            .parseLenient()
056            .appendPattern("uuuu-MM-dd['T'HH:mm:ss][.")
057            .appendValue(ChronoField.MILLI_OF_SECOND, 3, 3, SignStyle.NEVER).optionalEnd()
058            .appendPattern("[XXXXX]")
059            .toFormatter();
060                                                                                                    
061    private DateTimeUtil() {
062    }
063
064    /**
065     * Creates a {@link ZonedDateTime} instance based on the given pattern in ISO 8601 format, compatible with the Joda date-time
066     * library. 
067     * <p>
068     * Note that there is no direct correspondence between the JODA-style dates and the new JDK 8 date, especially 
069     * when it comes to handling milliseconds.
070     * </p>
071     * 
072     * @param iso8601 a {@link String} representing a date and/or time pattern, may not be null
073     * @return a {@link ZonedDateTime} instance, never {@code null}
074     * 
075     * @throws java.time.format.DateTimeParseException if the given pattern cannot be parsed
076     */
077    public static ZonedDateTime jodaParse( String iso8601 ) throws DateTimeParseException {
078        CheckArg.isNotNull(iso8601, "iso8601");
079        TemporalAccessor parse = JODA_ISO8601_FORMATTER.parse(iso8601);
080        LocalDate localDate = LocalDate.from(parse);
081        LocalTime localTime = parse.isSupported(ChronoField.HOUR_OF_DAY) ? LocalTime.from(parse) : LocalTime.MIDNIGHT;
082        ZoneId zoneId = parse.isSupported(ChronoField.OFFSET_SECONDS) ? ZoneId.from(parse) : UTC;
083        return ZonedDateTime.of(localDate, localTime, zoneId);    
084    }
085
086    /**
087     * Returns the ISO8601 string of a given date-time instance with timezone information, trying to be as closed as possible
088     * to what the JODA date-time library would return.
089     * 
090     * @param dateTime a {@link ZonedDateTime} instance, may not be null
091     * @return a {@link String} representation of the date instance according to the ISO8601 standard
092     */
093    public static String jodaFormat( ZonedDateTime dateTime ) {
094        CheckArg.isNotNull(dateTime, "dateTime");
095        return dateTime.format(JODA_ISO8601_FORMATTER);
096    }
097
098    /**
099     * Creates a new UTC {@link LocalDateTime} instance  based on the given millis value
100     *
101     * @param millis a positive amount of millis
102     * @return a {@link LocalDateTime} instance.
103     */
104    public static LocalDateTime localDateTimeUTC( long millis ) {
105        CheckArg.isPositive(millis, "millis");
106        return LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), UTC);
107    }
108}