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