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}