001/*
002 * (C) Copyright 2019 Nuxeo (http://nuxeo.com/) and others.
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 *
016 * Contributors:
017 *     pierre
018 */
019package org.nuxeo.common.utils;
020
021import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
022import static java.time.format.DateTimeFormatter.ISO_ZONED_DATE_TIME;
023import static java.time.temporal.ChronoField.DAY_OF_MONTH;
024import static java.time.temporal.ChronoField.HOUR_OF_DAY;
025import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
026import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
027import static java.time.temporal.ChronoField.NANO_OF_SECOND;
028import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;
029
030import java.time.DateTimeException;
031import java.time.Instant;
032import java.time.LocalDate;
033import java.time.ZoneOffset;
034import java.time.ZonedDateTime;
035import java.time.format.DateTimeFormatter;
036import java.time.format.DateTimeFormatterBuilder;
037import java.time.temporal.TemporalAccessor;
038import java.util.Calendar;
039import java.util.Date;
040import java.util.GregorianCalendar;
041
042/**
043 * Java 8 time utilities.
044 *
045 * @since 11.1
046 */
047public class DateUtils {
048
049    public static final String ISODATETIME_GENERIC_PATTERN = "yyyy[-MM][-dd['T'HH[:mm[:ss[.SSS]]]]][XXX]";
050
051    public static final DateTimeFormatter ISO_ROBUST_DATE_TIME = robustOfPattern(ISODATETIME_GENERIC_PATTERN);
052
053    public static final DateTimeFormatter[] formatters = {
054            ISO_OFFSET_DATE_TIME,
055            ISO_ZONED_DATE_TIME,
056            ISO_ROBUST_DATE_TIME, };
057
058    public static final DateTimeFormatter ISO_DATE_ONLY = DateTimeFormatter.ofPattern("yyyy-MM-dd");
059
060    private DateUtils() {
061        // utility class
062    }
063
064    public static String formatISODateTime(Calendar calendar) {
065        return formatISODateTime(toZonedDateTime(calendar));
066    }
067
068    public static String formatISODateTime(Date date) {
069        return formatISODateTime(toZonedDateTime(date));
070    }
071
072    public static String formatISODateTime(ZonedDateTime zdt) {
073        return formatISODateTime(zdt, false);
074    }
075
076    public static String formatISODateTime(ZonedDateTime zdt, boolean dateOnly) {
077        if (zdt == null) {
078            return null;
079        }
080        if (dateOnly) {
081            return ISO_DATE_ONLY.format(zdt);
082        }
083        return ISO_ROBUST_DATE_TIME.format(zdt);
084    }
085
086    public static Date nowIfNull(Date date) {
087        return date == null ? new Date() : date;
088    }
089
090    public static Calendar nowIfNull(Calendar calendar) {
091        return calendar == null ? Calendar.getInstance() : calendar;
092    }
093
094    public static ZonedDateTime nowIfNull(ZonedDateTime zdt) {
095        return zdt == null ? ZonedDateTime.now() : zdt;
096    }
097
098    public static ZonedDateTime parseISODateTime(String string) {
099        return parse(string, formatters);
100    }
101
102    public static ZonedDateTime parse(String string, DateTimeFormatter... formatters) {
103        // workaround to allow space instead of T after the date part
104        if (string.length() > 10 && string.charAt(10) == ' ') {
105            char[] s = string.toCharArray();
106            s[10] = 'T';
107            string = new String(s);
108        }
109        for (DateTimeFormatter formatter : formatters) {
110            try {
111                TemporalAccessor ta = formatter.parseBest(string, ZonedDateTime::from, LocalDate::from);
112                if (ta instanceof ZonedDateTime) {
113                    return (ZonedDateTime) ta;
114                } else {
115                    return ((LocalDate) ta).atStartOfDay(ZoneOffset.UTC);
116                }
117            } catch (Exception e) {
118                // ignore, try another formatter
119            }
120        }
121        throw new DateTimeException("Could not parse '" + string + "'");
122    }
123
124    public static final DateTimeFormatter robustOfPattern(String pattern) {
125        return new DateTimeFormatterBuilder().appendPattern(pattern)
126                                             .parseDefaulting(MONTH_OF_YEAR, 1)
127                                             .parseDefaulting(DAY_OF_MONTH, 1)
128                                             .parseDefaulting(HOUR_OF_DAY, 0)
129                                             .parseDefaulting(MINUTE_OF_HOUR, 0)
130                                             .parseDefaulting(SECOND_OF_MINUTE, 0)
131                                             .parseDefaulting(NANO_OF_SECOND, 0)
132                                             .toFormatter()
133                                             .withZone(ZoneOffset.UTC);
134    }
135
136    public static Calendar toCalendar(Instant instant) {
137        if (instant == null) {
138            return null;
139        }
140        // an Instant is on UTC by definition
141        var zdt = ZonedDateTime.ofInstant(instant, ZoneOffset.UTC);
142        return GregorianCalendar.from(zdt);
143    }
144
145    public static Date toDate(ZonedDateTime zdt) {
146        if (zdt == null) {
147            return null;
148        }
149        return Date.from(zdt.toInstant());
150    }
151
152    public static Instant toInstant(Calendar calendar) {
153        if (calendar == null) {
154            return null;
155        }
156        return calendar.toInstant();
157    }
158
159    public static ZonedDateTime toZonedDateTime(Calendar calendar) {
160        if (calendar == null) {
161            return null;
162        }
163        return ZonedDateTime.ofInstant(calendar.toInstant(), ZoneOffset.UTC);
164    }
165
166    public static ZonedDateTime toZonedDateTime(Date date) {
167        if (date == null) {
168            return null;
169        }
170        return ZonedDateTime.ofInstant(date.toInstant(), ZoneOffset.UTC);
171    }
172
173}