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 date == null ? null : date.toInstant();
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 date == null ? null : asInstant(date).atZone(zone != null ? zone : ZoneOffset.UTC);
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 calendar.toInstant().atZone(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 ZonedDateTime.ofInstant(instant, Optional.ofNullable(zone).orElse(ZoneOffset.UTC));
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 this {@link Date} object to a {@link LocalDate}.
140     * 
141     * This returns a {@link LocalDate} with the same year, month and day as this {@link Date}.
142     * @param date The date object
143     * @param zone The zone
144     * @return the {@link LocalDate} part of this {@link Date}
145     */
146    public static LocalDate asLocalDate(Date date, ZoneId zone)
147    {
148        return asZonedDateTime(date, zone).toLocalDate();
149    }
150    
151    /**
152     * Converts this {@link Date} object to a {@link LocalDate}.
153     * 
154     * This returns a {@link LocalDate} with the same year, month and day as this {@link Date}.
155     * @param date The date object
156     * @return the {@link LocalDate} part of this {@link Date}
157     */
158    public static LocalDate asLocalDate(Date date)
159    {
160        return asLocalDate(date, ZoneOffset.UTC);
161    }
162    
163    /**
164     * Converts this {@link Calendar} object to a {@link LocalDate}. <br>
165     * <b>Warning</b>: this conversion looses the Calendar's time components.
166     * @param calendar the calendar
167     * @return the {@link LocalDate} object
168     */
169    public static LocalDate asLocalDate(Calendar calendar)
170    {
171        return LocalDate.of(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH) + 1, calendar.get(Calendar.DAY_OF_MONTH));
172    }
173    
174    /**
175     * Converts this {@link LocalDate} object to a {@link Date}.
176     * 
177     * @param localDate The local date object
178     * @return the {@link Date} part of this {@link LocalDate}
179     */
180    public static Date asDate(LocalDate localDate)
181    {
182        return asDate(localDate, ZoneOffset.UTC);
183    }
184    
185    /**
186     * Converts this {@link LocalDate} object to a {@link Date}.
187     * 
188     * @param localDate The local date object
189     * @param zone The zone. If <code>null</code>, UTC is used
190     * @return the {@link Date} part of this {@link LocalDate}
191     */
192    public static Date asDate(LocalDate localDate, ZoneId zone)
193    {
194        return localDate == null ? null : Date.from(localDate.atStartOfDay(zone != null ? zone : ZoneOffset.UTC).toInstant());
195    }
196
197    /**
198     * Converts this {@link ZonedDateTime} object to a {@link Date}.
199     * 
200     * @param zonedDateTime The local date time object
201     * @return the {@link Date} part of this {@link ZonedDateTime}
202     */
203    public static Date asDate(ZonedDateTime zonedDateTime)
204    {
205        return Date.from(zonedDateTime.toInstant());
206    }
207    
208    /**
209     * Converts this {@link ZonedDateTime} object to a {@link Calendar}, setting the time zone to UTC.
210     * @param zonedDateTime the zoned date time.
211     * @return the converted {@link Calendar} object.
212     */
213    public static Calendar asCalendar(ZonedDateTime zonedDateTime)
214    {
215        ZonedDateTime dateTimeOnDefaultZone = zonedDateTime.withZoneSameInstant(ZoneOffset.UTC);
216        return GregorianCalendar.from(dateTimeOnDefaultZone);
217    }
218    
219    /**
220     * Converts this {@link LocalDate} object to a {@link Calendar}.
221     * @param localDate the local date
222     * @return the {@link Calendar} object
223     */
224    public static Calendar asCalendar(LocalDate localDate)
225    {
226        ZonedDateTime zdt = localDate.atStartOfDay(ZoneOffset.UTC);
227        return GregorianCalendar.from(zdt);
228    }
229    
230    /**
231     * Format a duration for logs
232     * @param duration duration to log
233     * @return a string representing the duration
234     */
235    public static String formatDuration(Duration duration)
236    {
237        return formatDuration(duration.toMillis());
238    }
239    
240    /**
241     * Format a duration for logs
242     * @param duration miliseconds representing the duration
243     * @return a string representing the duration
244     */
245    public static String formatDuration(long duration)
246    {
247        StringBuilder sb = new StringBuilder();
248        long durationCopy = duration;
249        long ms = durationCopy % 1000;
250        durationCopy /= 1000;
251        long s = durationCopy % 60;
252        durationCopy /= 60;
253        long m = durationCopy % 60;
254        durationCopy /= 60;
255        long h = durationCopy % 24;
256        durationCopy /= 24;
257        
258        boolean showDays = durationCopy > 0;
259        boolean showHours = showDays || h > 0;
260        boolean showMinuts = showHours || m > 0;
261        boolean showSeconds = showMinuts || s > 0;
262
263        if (showDays)
264        {
265            sb.append(durationCopy);
266            sb.append("j ");
267        }
268        if (showHours)
269        {
270            sb.append(formatNumber(h, 2));
271            sb.append("h ");
272        }
273        if (showMinuts)
274        {
275            sb.append(formatNumber(m, 2));
276            sb.append("m ");
277        }
278        if (showSeconds)
279        {
280            sb.append(formatNumber(s, 2));
281            sb.append("s ");
282        }
283        sb.append(formatNumber(ms, 3));
284        sb.append("ms");
285        return sb.toString();
286    }
287    private static String formatNumber(long number, int nbNumbers)
288    {
289        String numberFormatted = String.valueOf(number);
290        while (numberFormatted.length() < nbNumbers)
291        {
292            numberFormatted = "0" + numberFormatted;
293        }
294        return numberFormatted;
295    }
296    
297    /**
298     * 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'. 
299     * This formatter is similar to {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME} but force 3-digits milliseconds.
300     * @return ISO date-time formatter
301     */
302    public static DateTimeFormatter getISODateTimeFormatter()
303    {
304        return __ISO_OFFSET_DATE_TIME;
305    }
306    
307    /**
308     * Converts a {@link Date} object to {@link String} using the ISO date formatter, at UTC time zone.
309     * @param value the value to convert
310     * @return the date as a {@link String}
311     */
312    public static String dateToString(Date value)
313    {
314        if (value == null)
315        {
316            return null;
317        }
318        
319        ZonedDateTime zdt = DateUtils.asZonedDateTime(value, null);           
320        return zonedDateTimeToString(zdt);
321    }
322    
323    /**
324     * Converts this epoch time to a {@link String} using the ISO date formatter
325     * @param epochMilli the number of milliseconds from 1970-01-01T00:00:00Z
326     * @return the epoch time to a {@link String}
327     */
328    public static String epochMilliToString(long epochMilli)
329    {
330        ZonedDateTime zdt = DateUtils.asZonedDateTime(epochMilli, ZoneOffset.UTC);           
331        return zonedDateTimeToString(zdt);
332    }
333    
334    /**
335     * Converts a {@link ZonedDateTime} object to {@link String} using the {@link #__ISO_OFFSET_DATE_TIME ISO date formatter}
336     * @param zonedDateTime the zoned date time
337     * @return the zoned date time as a {@link String}
338     */
339    public static String zonedDateTimeToString(ZonedDateTime zonedDateTime)
340    {
341        return zonedDateTime.format(getISODateTimeFormatter());
342    }
343    
344    /**
345     * Converts a {@link ZonedDateTime} object to {@link String} using the given pattern for formatting, 
346     * in the given zone, so as to format the instant in another zone
347     * <br>For instance, if provided pattern is 'uuuu-MM-dd'T'HH:mm:ss.SSSXXX': 
348     * <ul>
349     * <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>
350     * <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>
351     * </ul>
352     * @param zonedDateTime the zoned date time
353     * @param zoneId the target zone
354     * @param pattern the pattern for formatting
355     * @return the zoned date time as a {@link String}, in the given zone
356     * @throws IllegalArgumentException  if the pattern is invalid
357     */
358    public static String zonedDateTimeToString(ZonedDateTime zonedDateTime, ZoneId zoneId, String pattern) throws IllegalArgumentException
359    {
360        ZonedDateTime sameInstantAtZone = zonedDateTime.withZoneSameInstant(zoneId);
361        DateTimeFormatter formatter = _createFormatter(pattern);
362        return sameInstantAtZone.format(formatter);
363    }
364    
365    /**
366     * Converts a {@link LocalDate} object to {@link String} using the ISO date formatter
367     * @param localDate the local date
368     * @return the local date as a {@link String}
369     */
370    public static String localDateToString(LocalDate localDate)
371    {
372        return localDate.format(DateTimeFormatter.ISO_LOCAL_DATE);
373    }
374    
375    /**
376     * Parses a String into a {@link Date}, using ISO 8601 format.
377     * @param value an ISO 8601 formatted String.
378     * @return the corresponding Date, or null if the input is null.
379     */
380    public static Date parse(String value)
381    {
382        if (StringUtils.isEmpty(value))
383        {
384            return null;
385        }
386        
387        ZonedDateTime zdt = ZonedDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME);
388        return asDate(zdt);
389    }
390    
391    /**
392     * Parses a String into a {@link ZonedDateTime}, using ISO date formatter.
393     * @param zonedDateTimeAsString the zoned date time as string
394     * @return the {@link ZonedDateTime} object or null if the input is null.
395     */
396    public static ZonedDateTime parseZonedDateTime(String zonedDateTimeAsString)
397    {
398        if (StringUtils.isEmpty(zonedDateTimeAsString))
399        {
400            return null;
401        }
402        
403        return ZonedDateTime.parse(zonedDateTimeAsString, DateTimeFormatter.ISO_DATE_TIME);
404    }
405    
406    /**
407     * Parses a String into a {@link LocalDate}, using ISO date formatter.
408     * @param localDateAsString the local date as string
409     * @return the {@link LocalDate} object or null if the input is null.
410     */
411    public static LocalDate parseLocalDate(String localDateAsString)
412    {
413        if (StringUtils.isEmpty(localDateAsString))
414        {
415            return null;
416        }
417        
418        return LocalDate.parse(localDateAsString, DateTimeFormatter.ISO_LOCAL_DATE);
419    }
420}