001/*
002 *  Copyright 2017 Anyware Services
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.ametys.core.util;
017
018import java.time.Duration;
019import java.time.Instant;
020import java.time.LocalDate;
021import java.time.ZoneId;
022import java.time.ZoneOffset;
023import java.time.ZonedDateTime;
024import java.time.chrono.IsoChronology;
025import java.time.format.DateTimeFormatter;
026import java.time.format.ResolverStyle;
027import java.util.Calendar;
028import java.util.Date;
029import java.util.GregorianCalendar;
030import java.util.Optional;
031
032import org.apache.commons.lang3.StringUtils;
033
034/**
035 * Helper for converting dates from the old ({@link Date}) to the new ({@link java.time}) JDK
036 * Special thanks to http://stackoverflow.com/questions/21242110/convert-java-util-date-to-java-time-localdate#answer-27378709
037 * which inspired this code
038 * 
039 * See also http://stackoverflow.com/questions/19431234/converting-between-java-time-localdatetime-and-java-util-date
040 */
041public final class DateUtils
042{
043    /** The ISO date-time pattern that formats or parses a date-time with an offset */
044    public static final String ISO_OFFSET_DATE_TIME_PATTERN = "uuuu-MM-dd'T'HH:mm:ss.SSSXXX";
045    
046    /**
047     * The ISO date-time formatter that formats or parses a date-time with an offset, such as '2011-12-03T10:15:30.000+01:00'. 
048     */
049    private static final DateTimeFormatter __ISO_OFFSET_DATE_TIME = _createFormatter(ISO_OFFSET_DATE_TIME_PATTERN);
050    
051    private DateUtils()
052    {
053        // empty
054    }
055    
056    private static DateTimeFormatter _createFormatter(String pattern) throws IllegalArgumentException
057    {
058        return DateTimeFormatter.ofPattern(pattern)
059                .withResolverStyle(ResolverStyle.STRICT)
060                .withChronology(IsoChronology.INSTANCE);
061    }
062    
063    /**
064     * Converts this {@link Date} object to an {@link Instant}.
065     * @param date The date object
066     * @return an instant representing the same point on the time-line as this {@link Date} object
067     */
068    public static Instant asInstant(Date date)
069    {
070        return Optional.ofNullable(date).map(Date::toInstant).orElse(null);
071    }
072    
073    /**
074     * Converts this {@link Date} object to a {@link ZonedDateTime}, at UTC.
075     * @param date The date object
076     * @return the {@link ZonedDateTime} formed from this {@link Date}
077     */
078    public static ZonedDateTime asZonedDateTime(Date date)
079    {
080        return asZonedDateTime(date, null);
081    }
082    
083    /**
084     * Converts this {@link Date} object to a {@link ZonedDateTime}.
085     * @param date The date object
086     * @param zone The zone. If <code>null</code>, UTC is used
087     * @return the {@link ZonedDateTime} formed from this {@link Date}
088     */
089    public static ZonedDateTime asZonedDateTime(Date date, ZoneId zone)
090    {
091        return asZonedDateTime(asInstant(date), zone);
092    }
093    
094    /**
095     * Converts this {@link Calendar} object to a {@link ZonedDateTime}.
096     * @param calendar the calendar
097     * @return the {@link ZonedDateTime} formed from this {@link Calendar}
098     */
099    public static ZonedDateTime asZonedDateTime(Calendar calendar)
100    {
101        return asZonedDateTime(calendar.toInstant(), ZoneOffset.UTC);
102    }
103    
104    /**
105     * Converts this epoch time to a {@link ZonedDateTime}.
106     * @param epochMilli the number of milliseconds from 1970-01-01T00:00:00Z
107     * @return the {@link ZonedDateTime} formed from this epoch time
108     */
109    public static ZonedDateTime asZonedDateTime(long epochMilli)
110    {
111        return asZonedDateTime(epochMilli, null);
112    }
113    
114    /**
115     * Converts this epoch time to a {@link ZonedDateTime}.
116     * @param epochMilli the number of milliseconds from 1970-01-01T00:00:00Z
117     * @param zone the time-zone
118     * @return the {@link ZonedDateTime} formed from this epoch time
119     */
120    public static ZonedDateTime asZonedDateTime(long epochMilli, ZoneId zone)
121    {
122        Instant instant = Instant.ofEpochMilli(epochMilli);
123        return asZonedDateTime(instant, zone);
124    }
125    
126    /**
127     * Converts a {@link LocalDate} to a {@link ZonedDateTime}
128     * @param localDate the local date
129     * @param zone The time zone. If <code>null</code>, UTC is used
130     * @return the {@link ZonedDateTime} formed from this local date
131     */
132    public static ZonedDateTime asZonedDateTime(LocalDate localDate, ZoneId zone)
133    {
134        Date date = asDate(localDate, zone);
135        return asZonedDateTime(date);
136    }
137    
138    /**
139     * Converts an {@link Instant} to a {@link ZonedDateTime}
140     * @param instant the instant
141     * @return the {@link ZonedDateTime} formed from this local date
142     */
143    public static ZonedDateTime asZonedDateTime(Instant instant)
144    {
145        return asZonedDateTime(instant, null);
146    }
147    
148    /**
149     * Converts an {@link Instant} to a {@link ZonedDateTime}
150     * @param instant the instant
151     * @param zone The time zone. If <code>null</code>, UTC is used
152     * @return the {@link ZonedDateTime} formed from this local date
153     */
154    public static ZonedDateTime asZonedDateTime(Instant instant, ZoneId zone)
155    {
156        return Optional.ofNullable(instant)
157            .map(i -> i.atZone(Optional.ofNullable(zone).orElse(ZoneOffset.UTC)))
158            .orElse(null);
159    }
160    
161    /**
162     * Converts this {@link Date} object to a {@link LocalDate}.
163     * 
164     * This returns a {@link LocalDate} with the same year, month and day as this {@link Date}.
165     * @param date The date object
166     * @param zone The zone
167     * @return the {@link LocalDate} part of this {@link Date}
168     */
169    public static LocalDate asLocalDate(Date date, ZoneId zone)
170    {
171        return asZonedDateTime(date, zone).toLocalDate();
172    }
173    
174    /**
175     * Converts this {@link Date} object to a {@link LocalDate}.
176     * 
177     * This returns a {@link LocalDate} with the same year, month and day as this {@link Date}.
178     * @param date The date object
179     * @return the {@link LocalDate} part of this {@link Date}
180     */
181    public static LocalDate asLocalDate(Date date)
182    {
183        return asLocalDate(date, null);
184    }
185    
186    /**
187     * Converts this {@link Calendar} object to a {@link LocalDate}. <br>
188     * <b>Warning</b>: this conversion looses the Calendar's time components.
189     * @param calendar the calendar
190     * @return the {@link LocalDate} object
191     */
192    public static LocalDate asLocalDate(Calendar calendar)
193    {
194        return LocalDate.of(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH) + 1, calendar.get(Calendar.DAY_OF_MONTH));
195    }
196    
197    /**
198     * Converts an {@link Instant} to a {@link LocalDate}
199     * @param instant the instant
200     * @return the {@link LocalDate} formed from this local date
201     */
202    public static LocalDate asLocalDate(Instant instant)
203    {
204        return asLocalDate(instant, null);
205    }
206    
207    /**
208     * Converts an {@link Instant} to a {@link LocalDate}
209     * @param instant the instant
210     * @param zone The time zone. If <code>null</code>, UTC is used
211     * @return the {@link LocalDate} formed from this local date
212     */
213    public static LocalDate asLocalDate(Instant instant, ZoneId zone)
214    {
215        return LocalDate.ofInstant(instant, Optional.ofNullable(zone).orElse(ZoneOffset.UTC));
216    }
217    
218    /**
219     * Converts this {@link LocalDate} object to a {@link Date}.
220     * 
221     * @param localDate The local date object
222     * @return the {@link Date} part of this {@link LocalDate}
223     */
224    public static Date asDate(LocalDate localDate)
225    {
226        return asDate(localDate, ZoneOffset.UTC);
227    }
228    
229    /**
230     * Converts this {@link LocalDate} object to a {@link Date}.
231     * 
232     * @param localDate The local date object
233     * @param zone The zone. If <code>null</code>, UTC is used
234     * @return the {@link Date} part of this {@link LocalDate}
235     */
236    public static Date asDate(LocalDate localDate, ZoneId zone)
237    {
238        return Optional.ofNullable(localDate)
239                .map(ld -> ld.atStartOfDay(Optional.ofNullable(zone).orElse(ZoneOffset.UTC)))
240                .map(ZonedDateTime::toInstant)
241                .map(Date::from)
242                .orElse(null);
243    }
244
245    /**
246     * Converts this {@link ZonedDateTime} object to a {@link Date}.
247     * 
248     * @param zonedDateTime The local date time object
249     * @return the {@link Date} part of this {@link ZonedDateTime}
250     */
251    public static Date asDate(ZonedDateTime zonedDateTime)
252    {
253        return Date.from(zonedDateTime.toInstant());
254    }
255    
256    /**
257     * Converts this {@link ZonedDateTime} object to a {@link Calendar}, setting the time zone to UTC.
258     * @param zonedDateTime the zoned date time.
259     * @return the converted {@link Calendar} object.
260     */
261    public static Calendar asCalendar(ZonedDateTime zonedDateTime)
262    {
263        ZonedDateTime dateTimeOnDefaultZone = zonedDateTime.withZoneSameInstant(ZoneOffset.UTC);
264        return GregorianCalendar.from(dateTimeOnDefaultZone);
265    }
266    
267    /**
268     * Converts this {@link LocalDate} object to a {@link Calendar}.
269     * @param localDate the local date
270     * @return the {@link Calendar} object
271     */
272    public static Calendar asCalendar(LocalDate localDate)
273    {
274        ZonedDateTime zdt = localDate.atStartOfDay(ZoneOffset.UTC);
275        return GregorianCalendar.from(zdt);
276    }
277    
278    /**
279     * Format a duration for logs
280     * @param duration duration to log
281     * @return a string representing the duration
282     */
283    public static String formatDuration(Duration duration)
284    {
285        return formatDuration(duration.toMillis());
286    }
287    
288    /**
289     * Format a duration for logs
290     * @param duration miliseconds representing the duration
291     * @return a string representing the duration
292     */
293    public static String formatDuration(long duration)
294    {
295        StringBuilder sb = new StringBuilder();
296        long durationCopy = duration;
297        long ms = durationCopy % 1000;
298        durationCopy /= 1000;
299        long s = durationCopy % 60;
300        durationCopy /= 60;
301        long m = durationCopy % 60;
302        durationCopy /= 60;
303        long h = durationCopy % 24;
304        durationCopy /= 24;
305        
306        boolean showDays = durationCopy > 0;
307        boolean showHours = showDays || h > 0;
308        boolean showMinuts = showHours || m > 0;
309        boolean showSeconds = showMinuts || s > 0;
310
311        if (showDays)
312        {
313            sb.append(durationCopy);
314            sb.append("j ");
315        }
316        if (showHours)
317        {
318            sb.append(formatNumber(h, 2));
319            sb.append("h ");
320        }
321        if (showMinuts)
322        {
323            sb.append(formatNumber(m, 2));
324            sb.append("m ");
325        }
326        if (showSeconds)
327        {
328            sb.append(formatNumber(s, 2));
329            sb.append("s ");
330        }
331        sb.append(formatNumber(ms, 3));
332        sb.append("ms");
333        return sb.toString();
334    }
335    private static String formatNumber(long number, int nbNumbers)
336    {
337        String numberFormatted = String.valueOf(number);
338        while (numberFormatted.length() < nbNumbers)
339        {
340            numberFormatted = "0" + numberFormatted;
341        }
342        return numberFormatted;
343    }
344    
345    /**
346     * Get the ISO date-time formatter that formats or parses a date-time with an offset, such as '2011-12-03T10:15:30.000+01:00'. 
347     * This formatter is similar to {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME} but force 3-digits milliseconds.
348     * @return ISO date-time formatter
349     */
350    public static DateTimeFormatter getISODateTimeFormatter()
351    {
352        return __ISO_OFFSET_DATE_TIME;
353    }
354    
355    /**
356     * Converts a {@link Date} object to {@link String} using the ISO date formatter, at UTC time zone.
357     * @param value the value to convert
358     * @return the date as a {@link String}
359     */
360    public static String dateToString(Date value)
361    {
362        if (value == null)
363        {
364            return null;
365        }
366        
367        ZonedDateTime zdt = DateUtils.asZonedDateTime(value, null);
368        return zonedDateTimeToString(zdt);
369    }
370    
371    /**
372     * Converts this epoch time to a {@link String} using the ISO date formatter
373     * @param epochMilli the number of milliseconds from 1970-01-01T00:00:00Z
374     * @return the epoch time to a {@link String}
375     */
376    public static String epochMilliToString(long epochMilli)
377    {
378        ZonedDateTime zdt = DateUtils.asZonedDateTime(epochMilli, ZoneOffset.UTC);
379        return zonedDateTimeToString(zdt);
380    }
381    
382    /**
383     * Converts a {@link ZonedDateTime} object to {@link String} using the {@link #__ISO_OFFSET_DATE_TIME ISO date formatter}
384     * @param zonedDateTime the zoned date time
385     * @return the zoned date time as a {@link String}
386     */
387    public static String zonedDateTimeToString(ZonedDateTime zonedDateTime)
388    {
389        return zonedDateTime.format(getISODateTimeFormatter());
390    }
391    
392    /**
393     * Converts a {@link ZonedDateTime} object to {@link String} using the given pattern for formatting, 
394     * in the given zone, so as to format the instant in another zone
395     * <br>For instance, if provided pattern is 'uuuu-MM-dd'T'HH:mm:ss.SSSXXX': 
396     * <ul>
397     * <li>for UTC zone, the zone date time corresponding to '2011-12-03T11:15:30+01:00' will be formatted as '2011-12-03T10:15:30Z'</li>
398     * <li>for +02:00 zone, the zone date time corresponding to '2011-12-03T11:15:30+01:00' will be formatted as '2011-12-03T12:15:30+02:00'</li>
399     * </ul>
400     * @param zonedDateTime the zoned date time
401     * @param zoneId the target zone
402     * @param pattern the pattern for formatting
403     * @return the zoned date time as a {@link String}, in the given zone
404     * @throws IllegalArgumentException  if the pattern is invalid
405     */
406    public static String zonedDateTimeToString(ZonedDateTime zonedDateTime, ZoneId zoneId, String pattern) throws IllegalArgumentException
407    {
408        ZonedDateTime sameInstantAtZone = zonedDateTime.withZoneSameInstant(zoneId);
409        DateTimeFormatter formatter = _createFormatter(pattern);
410        return sameInstantAtZone.format(formatter);
411    }
412    
413    /**
414     * Converts a {@link LocalDate} object to {@link String} using the ISO date formatter
415     * @param localDate the local date
416     * @return the local date as a {@link String}
417     */
418    public static String localDateToString(LocalDate localDate)
419    {
420        return localDate.format(DateTimeFormatter.ISO_LOCAL_DATE);
421    }
422    
423    /**
424     * Parses a String into a {@link Date}, using ISO 8601 format.
425     * @param value an ISO 8601 formatted String.
426     * @return the corresponding Date, or null if the input is null.
427     */
428    public static Date parse(String value)
429    {
430        if (StringUtils.isEmpty(value))
431        {
432            return null;
433        }
434        
435        ZonedDateTime zdt = ZonedDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME);
436        return asDate(zdt);
437    }
438    
439    /**
440     * Parses a String into a {@link ZonedDateTime}, using ISO date time formatter.
441     * @param zonedDateTimeAsString the zoned date time as string
442     * @return the {@link ZonedDateTime} object or null if the input is null.
443     */
444    public static ZonedDateTime parseZonedDateTime(String zonedDateTimeAsString)
445    {
446        return parseZonedDateTime(zonedDateTimeAsString, Optional.empty());
447    }
448    
449    /**
450     * Parses a String into a {@link ZonedDateTime}, using the given formatter.
451     * If no formatter, the ISO date time formatter is used
452     * @param zonedDateTimeAsString the zoned date time as string
453     * @param formatter the date time formatter
454     * @return the {@link ZonedDateTime} object or null if the input is null.
455     */
456    public static ZonedDateTime parseZonedDateTime(String zonedDateTimeAsString, Optional<DateTimeFormatter> formatter)
457    {
458        if (StringUtils.isEmpty(zonedDateTimeAsString))
459        {
460            return null;
461        }
462        
463        return ZonedDateTime.parse(zonedDateTimeAsString, formatter.orElse(DateTimeFormatter.ISO_DATE_TIME));
464    }
465    
466    /**
467     * Parses a String into a {@link LocalDate}, using ISO local date formatter.
468     * @param localDateAsString the local date as string
469     * @return the {@link LocalDate} object or null if the input is null.
470     */
471    public static LocalDate parseLocalDate(String localDateAsString)
472    {
473        return parseLocalDate(localDateAsString, Optional.empty());
474    }
475    
476    /**
477     * Parses a String into a {@link LocalDate}, using the given formatter.
478     * If no formatter, the ISO local date formatter is used
479     * @param localDateAsString the local date as string
480     * @param formatter the date time formatter
481     * @return the {@link LocalDate} object or null if the input is null.
482     */
483    public static LocalDate parseLocalDate(String localDateAsString, Optional<DateTimeFormatter> formatter)
484    {
485        if (StringUtils.isEmpty(localDateAsString))
486        {
487            return null;
488        }
489        
490        return LocalDate.parse(localDateAsString, formatter.orElse(DateTimeFormatter.ISO_LOCAL_DATE));
491    }
492}