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