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 this epoch time to a {@link LocalDate}.
199     * @param epochMilli the number of milliseconds from 1970-01-01T00:00:00Z
200     * @return the {@link LocalDate} formed from this epoch time
201     */
202    public static LocalDate asLocalDate(long epochMilli)
203    {
204        return asLocalDate(epochMilli, null);
205    }
206    
207    /**
208     * Converts this epoch time to a {@link LocalDate}.
209     * @param epochMilli the number of milliseconds from 1970-01-01T00:00:00Z
210     * @param zone The time zone. If <code>null</code>, UTC is used
211     * @return the {@link LocalDate} formed from this epoch time
212     */
213    public static LocalDate asLocalDate(long epochMilli, ZoneId zone)
214    {
215        Instant instant = Instant.ofEpochMilli(epochMilli);
216        return asLocalDate(instant, zone);
217    }
218    
219    /**
220     * Converts an {@link Instant} to a {@link LocalDate}
221     * @param instant the instant
222     * @return the {@link LocalDate} formed from this local date
223     */
224    public static LocalDate asLocalDate(Instant instant)
225    {
226        return asLocalDate(instant, null);
227    }
228    
229    /**
230     * Converts an {@link Instant} to a {@link LocalDate}
231     * @param instant the instant
232     * @param zone The time zone. If <code>null</code>, UTC is used
233     * @return the {@link LocalDate} formed from this local date
234     */
235    public static LocalDate asLocalDate(Instant instant, ZoneId zone)
236    {
237        return LocalDate.ofInstant(instant, Optional.ofNullable(zone).orElse(ZoneOffset.UTC));
238    }
239    
240    /**
241     * Converts this {@link LocalDate} object to a {@link Date}.
242     * 
243     * @param localDate The local date object
244     * @return the {@link Date} part of this {@link LocalDate}
245     */
246    public static Date asDate(LocalDate localDate)
247    {
248        return asDate(localDate, ZoneOffset.UTC);
249    }
250    
251    /**
252     * Converts this {@link LocalDate} object to a {@link Date}.
253     * 
254     * @param localDate The local date object
255     * @param zone The zone. If <code>null</code>, UTC is used
256     * @return the {@link Date} part of this {@link LocalDate}
257     */
258    public static Date asDate(LocalDate localDate, ZoneId zone)
259    {
260        return Optional.ofNullable(localDate)
261                .map(ld -> ld.atStartOfDay(Optional.ofNullable(zone).orElse(ZoneOffset.UTC)))
262                .map(ZonedDateTime::toInstant)
263                .map(Date::from)
264                .orElse(null);
265    }
266
267    /**
268     * Converts this {@link ZonedDateTime} object to a {@link Date}.
269     * 
270     * @param zonedDateTime The local date time object
271     * @return the {@link Date} part of this {@link ZonedDateTime}
272     */
273    public static Date asDate(ZonedDateTime zonedDateTime)
274    {
275        return Date.from(zonedDateTime.toInstant());
276    }
277    
278    /**
279     * Converts this {@link ZonedDateTime} object to a {@link Calendar}, setting the time zone to UTC.
280     * @param zonedDateTime the zoned date time.
281     * @return the converted {@link Calendar} object.
282     */
283    public static Calendar asCalendar(ZonedDateTime zonedDateTime)
284    {
285        ZonedDateTime dateTimeOnDefaultZone = zonedDateTime.withZoneSameInstant(ZoneOffset.UTC);
286        return GregorianCalendar.from(dateTimeOnDefaultZone);
287    }
288    
289    /**
290     * Converts this {@link LocalDate} object to a {@link Calendar}.
291     * @param localDate the local date
292     * @return the {@link Calendar} object
293     */
294    public static Calendar asCalendar(LocalDate localDate)
295    {
296        ZonedDateTime zdt = localDate.atStartOfDay(ZoneOffset.UTC);
297        return GregorianCalendar.from(zdt);
298    }
299    
300    /**
301     * Format a duration for logs
302     * @param duration duration to log
303     * @return a string representing the duration
304     */
305    public static String formatDuration(Duration duration)
306    {
307        return formatDuration(duration.toMillis());
308    }
309    
310    /**
311     * Format a duration for logs
312     * @param duration miliseconds representing the duration
313     * @return a string representing the duration
314     */
315    public static String formatDuration(long duration)
316    {
317        StringBuilder sb = new StringBuilder();
318        long durationCopy = duration;
319        long ms = durationCopy % 1000;
320        durationCopy /= 1000;
321        long s = durationCopy % 60;
322        durationCopy /= 60;
323        long m = durationCopy % 60;
324        durationCopy /= 60;
325        long h = durationCopy % 24;
326        durationCopy /= 24;
327        
328        boolean showDays = durationCopy > 0;
329        boolean showHours = showDays || h > 0;
330        boolean showMinuts = showHours || m > 0;
331        boolean showSeconds = showMinuts || s > 0;
332
333        if (showDays)
334        {
335            sb.append(durationCopy);
336            sb.append("j ");
337        }
338        if (showHours)
339        {
340            sb.append(formatNumber(h, 2));
341            sb.append("h ");
342        }
343        if (showMinuts)
344        {
345            sb.append(formatNumber(m, 2));
346            sb.append("m ");
347        }
348        if (showSeconds)
349        {
350            sb.append(formatNumber(s, 2));
351            sb.append("s ");
352        }
353        sb.append(formatNumber(ms, 3));
354        sb.append("ms");
355        return sb.toString();
356    }
357    private static String formatNumber(long number, int nbNumbers)
358    {
359        String numberFormatted = String.valueOf(number);
360        while (numberFormatted.length() < nbNumbers)
361        {
362            numberFormatted = "0" + numberFormatted;
363        }
364        return numberFormatted;
365    }
366    
367    /**
368     * 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'. 
369     * This formatter is similar to {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME} but force 3-digits milliseconds.
370     * @return ISO date-time formatter
371     */
372    public static DateTimeFormatter getISODateTimeFormatter()
373    {
374        return __ISO_OFFSET_DATE_TIME;
375    }
376    
377    /**
378     * Converts a {@link Date} object to {@link String} using the ISO date formatter, at UTC time zone.
379     * @param value the value to convert
380     * @return the date as a {@link String}
381     */
382    public static String dateToString(Date value)
383    {
384        if (value == null)
385        {
386            return null;
387        }
388        
389        ZonedDateTime zdt = DateUtils.asZonedDateTime(value, null);
390        return zonedDateTimeToString(zdt);
391    }
392    
393    /**
394     * Converts this epoch time to a {@link String} using the ISO date formatter
395     * @param epochMilli the number of milliseconds from 1970-01-01T00:00:00Z
396     * @return the epoch time to a {@link String}
397     */
398    public static String epochMilliToString(long epochMilli)
399    {
400        ZonedDateTime zdt = DateUtils.asZonedDateTime(epochMilli, ZoneOffset.UTC);
401        return zonedDateTimeToString(zdt);
402    }
403    
404    /**
405     * Converts a {@link ZonedDateTime} object to {@link String} in the given zone, so as to format the instant in another zone,
406     * using the ISO date time formatter with pattern 'uuuu-MM-dd'T'HH:mm:ss.SSSXXX'. For instance: 
407     * <ul>
408     * <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>
409     * <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>
410     * </ul>
411     * @param zonedDateTime the zoned date time
412     * @param zoneId the target zone
413     * @return the zoned date time as a {@link String}, in the given zone
414     */
415    public static String zonedDateTimeToString(ZonedDateTime zonedDateTime, ZoneId zoneId)
416    {
417        ZonedDateTime sameInstantAtZone = zonedDateTime.withZoneSameInstant(zoneId);
418        return zonedDateTimeToString(sameInstantAtZone);
419    }
420    
421    /**
422     * Converts a {@link ZonedDateTime} object to {@link String} using the {@link #__ISO_OFFSET_DATE_TIME ISO date formatter}
423     * @param zonedDateTime the zoned date time
424     * @return the zoned date time as a {@link String}
425     */
426    public static String zonedDateTimeToString(ZonedDateTime zonedDateTime)
427    {
428        return zonedDateTime.format(getISODateTimeFormatter());
429    }
430    
431    /**
432     * Converts a {@link ZonedDateTime} object to {@link String} using the given pattern for formatting, 
433     * in the given zone, so as to format the instant in another zone
434     * <br>For instance, if provided pattern is 'uuuu-MM-dd'T'HH:mm:ss.SSSXXX': 
435     * <ul>
436     * <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>
437     * <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>
438     * </ul>
439     * @param zonedDateTime the zoned date time
440     * @param zoneId the target zone
441     * @param pattern the pattern for formatting
442     * @return the zoned date time as a {@link String}, in the given zone
443     * @throws IllegalArgumentException  if the pattern is invalid
444     */
445    public static String zonedDateTimeToString(ZonedDateTime zonedDateTime, ZoneId zoneId, String pattern) throws IllegalArgumentException
446    {
447        ZonedDateTime sameInstantAtZone = zonedDateTime.withZoneSameInstant(zoneId);
448        DateTimeFormatter formatter = _createFormatter(pattern);
449        return sameInstantAtZone.format(formatter);
450    }
451    
452    /**
453     * Converts a {@link LocalDate} object to {@link String} using the ISO date formatter
454     * @param localDate the local date
455     * @return the local date as a {@link String}
456     */
457    public static String localDateToString(LocalDate localDate)
458    {
459        return localDate.format(DateTimeFormatter.ISO_LOCAL_DATE);
460    }
461    
462    /**
463     * Converts a {@link LocalDate} object to {@link String} using the given pattern for formatting
464     * @param localDate the local date
465     * @param pattern the pattern for formatting
466     * @return the local date as a {@link String}
467     * @throws IllegalArgumentException  if the pattern is invalid
468     */
469    public static String localDateToString(LocalDate localDate, String pattern) throws IllegalArgumentException
470    {
471        DateTimeFormatter formatter = _createFormatter(pattern);
472        return localDate.format(formatter);
473    }
474    
475    /**
476     * Parses a String into a {@link Date}, using ISO 8601 format.
477     * @param value an ISO 8601 formatted String.
478     * @return the corresponding Date, or null if the input is null.
479     */
480    public static Date parse(String value)
481    {
482        if (StringUtils.isEmpty(value))
483        {
484            return null;
485        }
486        
487        ZonedDateTime zdt = ZonedDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME);
488        return asDate(zdt);
489    }
490    
491    /**
492     * Parses a String into a {@link ZonedDateTime}, using ISO date time formatter.
493     * @param zonedDateTimeAsString the zoned date time as string
494     * @return the {@link ZonedDateTime} object or null if the input is null.
495     */
496    public static ZonedDateTime parseZonedDateTime(String zonedDateTimeAsString)
497    {
498        return parseZonedDateTime(zonedDateTimeAsString, Optional.empty());
499    }
500    
501    /**
502     * Parses a String into a {@link ZonedDateTime}, using ISO date time formatter.
503     * @param zonedDateTimeAsString the zoned date time as string
504     * @param pattern the pattern to use to parse the given date
505     * @return the {@link ZonedDateTime} object or null if the input is null.
506     * @throws IllegalArgumentException  if the pattern is invalid
507     */
508    public static ZonedDateTime parseZonedDateTime(String zonedDateTimeAsString, String pattern) throws IllegalArgumentException
509    {
510        DateTimeFormatter formatter = _createFormatter(pattern);
511        return parseZonedDateTime(zonedDateTimeAsString, Optional.of(formatter));
512    }
513    
514    /**
515     * Parses a String into a {@link ZonedDateTime}, using the given formatter.
516     * If no formatter, the ISO date time formatter is used
517     * @param zonedDateTimeAsString the zoned date time as string
518     * @param formatter the date time formatter
519     * @return the {@link ZonedDateTime} object or null if the input is null.
520     */
521    public static ZonedDateTime parseZonedDateTime(String zonedDateTimeAsString, Optional<DateTimeFormatter> formatter)
522    {
523        if (StringUtils.isEmpty(zonedDateTimeAsString))
524        {
525            return null;
526        }
527        
528        return ZonedDateTime.parse(zonedDateTimeAsString, formatter.orElse(DateTimeFormatter.ISO_DATE_TIME));
529    }
530    
531    /**
532     * Parses a String into a {@link LocalDate}, using ISO local date formatter.
533     * @param localDateAsString the local date as string
534     * @return the {@link LocalDate} object or null if the input is null.
535     */
536    public static LocalDate parseLocalDate(String localDateAsString)
537    {
538        return parseLocalDate(localDateAsString, Optional.empty());
539    }
540    
541    /**
542     * Parses a String into a {@link LocalDate}, using ISO local date formatter.
543     * @param localDateAsString the local date as string
544     * @param pattern the pattern to use to parse the given date
545     * @return the {@link LocalDate} object or null if the input is null.
546     * @throws IllegalArgumentException  if the pattern is invalid
547     */
548    public static LocalDate parseLocalDate(String localDateAsString, String pattern) throws IllegalArgumentException
549    {
550        DateTimeFormatter formatter = _createFormatter(pattern);
551        return parseLocalDate(localDateAsString, Optional.of(formatter));
552    }
553    
554    /**
555     * Parses a String into a {@link LocalDate}, using the given formatter.
556     * If no formatter, the ISO local date formatter is used
557     * @param localDateAsString the local date as string
558     * @param formatter the date time formatter
559     * @return the {@link LocalDate} object or null if the input is null.
560     */
561    public static LocalDate parseLocalDate(String localDateAsString, Optional<DateTimeFormatter> formatter)
562    {
563        if (StringUtils.isEmpty(localDateAsString))
564        {
565            return null;
566        }
567        
568        return LocalDate.parse(localDateAsString, formatter.orElse(DateTimeFormatter.ISO_LOCAL_DATE));
569    }
570    
571    /**
572     * Determines if a date is at midnight (00:00) for the given time-zone
573     * @param zonedDateTimeAsString the zoned date time as string
574     * @param zoneId the time-zone id. If empty the system default time-zone will be used.
575     * @return true if the date is at midnight
576     */
577    public static boolean isAtMidnight(String zonedDateTimeAsString, String zoneId)
578    {
579        ZonedDateTime zonedDateTime = parseZonedDateTime(zonedDateTimeAsString);
580        zonedDateTime = zonedDateTime.withZoneSameInstant(StringUtils.isEmpty(zoneId) ? ZoneId.systemDefault() : ZoneId.of(zoneId));
581        return zonedDateTime.getHour() == 0 && zonedDateTime.getMinute() == 0;
582    }
583}