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} in the given zone, so as to format the instant in another zone,
384     * using the ISO date time formatter with pattern 'uuuu-MM-dd'T'HH:mm:ss.SSSXXX'. For instance: 
385     * <ul>
386     * <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>
387     * <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>
388     * </ul>
389     * @param zonedDateTime the zoned date time
390     * @param zoneId the target zone
391     * @return the zoned date time as a {@link String}, in the given zone
392     */
393    public static String zonedDateTimeToString(ZonedDateTime zonedDateTime, ZoneId zoneId)
394    {
395        ZonedDateTime sameInstantAtZone = zonedDateTime.withZoneSameInstant(zoneId);
396        return zonedDateTimeToString(sameInstantAtZone);
397    }
398    
399    /**
400     * Converts a {@link ZonedDateTime} object to {@link String} using the {@link #__ISO_OFFSET_DATE_TIME ISO date formatter}
401     * @param zonedDateTime the zoned date time
402     * @return the zoned date time as a {@link String}
403     */
404    public static String zonedDateTimeToString(ZonedDateTime zonedDateTime)
405    {
406        return zonedDateTime.format(getISODateTimeFormatter());
407    }
408    
409    /**
410     * Converts a {@link ZonedDateTime} object to {@link String} using the given pattern for formatting, 
411     * in the given zone, so as to format the instant in another zone
412     * <br>For instance, if provided pattern is 'uuuu-MM-dd'T'HH:mm:ss.SSSXXX': 
413     * <ul>
414     * <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>
415     * <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>
416     * </ul>
417     * @param zonedDateTime the zoned date time
418     * @param zoneId the target zone
419     * @param pattern the pattern for formatting
420     * @return the zoned date time as a {@link String}, in the given zone
421     * @throws IllegalArgumentException  if the pattern is invalid
422     */
423    public static String zonedDateTimeToString(ZonedDateTime zonedDateTime, ZoneId zoneId, String pattern) throws IllegalArgumentException
424    {
425        ZonedDateTime sameInstantAtZone = zonedDateTime.withZoneSameInstant(zoneId);
426        DateTimeFormatter formatter = _createFormatter(pattern);
427        return sameInstantAtZone.format(formatter);
428    }
429    
430    /**
431     * Converts a {@link LocalDate} object to {@link String} using the ISO date formatter
432     * @param localDate the local date
433     * @return the local date as a {@link String}
434     */
435    public static String localDateToString(LocalDate localDate)
436    {
437        return localDate.format(DateTimeFormatter.ISO_LOCAL_DATE);
438    }
439    
440    /**
441     * Converts a {@link LocalDate} object to {@link String} using the given pattern for formatting
442     * @param localDate the local date
443     * @param pattern the pattern for formatting
444     * @return the local date as a {@link String}
445     * @throws IllegalArgumentException  if the pattern is invalid
446     */
447    public static String localDateToString(LocalDate localDate, String pattern) throws IllegalArgumentException
448    {
449        DateTimeFormatter formatter = _createFormatter(pattern);
450        return localDate.format(formatter);
451    }
452    
453    /**
454     * Parses a String into a {@link Date}, using ISO 8601 format.
455     * @param value an ISO 8601 formatted String.
456     * @return the corresponding Date, or null if the input is null.
457     */
458    public static Date parse(String value)
459    {
460        if (StringUtils.isEmpty(value))
461        {
462            return null;
463        }
464        
465        ZonedDateTime zdt = ZonedDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME);
466        return asDate(zdt);
467    }
468    
469    /**
470     * Parses a String into a {@link ZonedDateTime}, using ISO date time formatter.
471     * @param zonedDateTimeAsString the zoned date time as string
472     * @return the {@link ZonedDateTime} object or null if the input is null.
473     */
474    public static ZonedDateTime parseZonedDateTime(String zonedDateTimeAsString)
475    {
476        return parseZonedDateTime(zonedDateTimeAsString, Optional.empty());
477    }
478    
479    /**
480     * Parses a String into a {@link ZonedDateTime}, using ISO date time formatter.
481     * @param zonedDateTimeAsString the zoned date time as string
482     * @param pattern the pattern to use to parse the given date
483     * @return the {@link ZonedDateTime} object or null if the input is null.
484     * @throws IllegalArgumentException  if the pattern is invalid
485     */
486    public static ZonedDateTime parseZonedDateTime(String zonedDateTimeAsString, String pattern) throws IllegalArgumentException
487    {
488        DateTimeFormatter formatter = _createFormatter(pattern);
489        return parseZonedDateTime(zonedDateTimeAsString, Optional.of(formatter));
490    }
491    
492    /**
493     * Parses a String into a {@link ZonedDateTime}, using the given formatter.
494     * If no formatter, the ISO date time formatter is used
495     * @param zonedDateTimeAsString the zoned date time as string
496     * @param formatter the date time formatter
497     * @return the {@link ZonedDateTime} object or null if the input is null.
498     */
499    public static ZonedDateTime parseZonedDateTime(String zonedDateTimeAsString, Optional<DateTimeFormatter> formatter)
500    {
501        if (StringUtils.isEmpty(zonedDateTimeAsString))
502        {
503            return null;
504        }
505        
506        return ZonedDateTime.parse(zonedDateTimeAsString, formatter.orElse(DateTimeFormatter.ISO_DATE_TIME));
507    }
508    
509    /**
510     * Parses a String into a {@link LocalDate}, using ISO local date formatter.
511     * @param localDateAsString the local date as string
512     * @return the {@link LocalDate} object or null if the input is null.
513     */
514    public static LocalDate parseLocalDate(String localDateAsString)
515    {
516        return parseLocalDate(localDateAsString, Optional.empty());
517    }
518    
519    /**
520     * Parses a String into a {@link LocalDate}, using ISO local date formatter.
521     * @param localDateAsString the local date as string
522     * @param pattern the pattern to use to parse the given date
523     * @return the {@link LocalDate} object or null if the input is null.
524     * @throws IllegalArgumentException  if the pattern is invalid
525     */
526    public static LocalDate parseLocalDate(String localDateAsString, String pattern) throws IllegalArgumentException
527    {
528        DateTimeFormatter formatter = _createFormatter(pattern);
529        return parseLocalDate(localDateAsString, Optional.of(formatter));
530    }
531    
532    /**
533     * Parses a String into a {@link LocalDate}, using the given formatter.
534     * If no formatter, the ISO local date formatter is used
535     * @param localDateAsString the local date as string
536     * @param formatter the date time formatter
537     * @return the {@link LocalDate} object or null if the input is null.
538     */
539    public static LocalDate parseLocalDate(String localDateAsString, Optional<DateTimeFormatter> formatter)
540    {
541        if (StringUtils.isEmpty(localDateAsString))
542        {
543            return null;
544        }
545        
546        return LocalDate.parse(localDateAsString, formatter.orElse(DateTimeFormatter.ISO_LOCAL_DATE));
547    }
548    
549    /**
550     * Determines if a date is at midnight (00:00) for the given time-zone
551     * @param zonedDateTimeAsString the zoned date time as string
552     * @param zoneId the time-zone id. If empty the system default time-zone will be used.
553     * @return true if the date is at midnight
554     */
555    public static boolean isAtMidnight(String zonedDateTimeAsString, String zoneId)
556    {
557        ZonedDateTime zonedDateTime = parseZonedDateTime(zonedDateTimeAsString);
558        zonedDateTime = zonedDateTime.withZoneSameInstant(StringUtils.isEmpty(zoneId) ? ZoneId.systemDefault() : ZoneId.of(zoneId));
559        return zonedDateTime.getHour() == 0 && zonedDateTime.getMinute() == 0;
560    }
561}