|
20 | 20 | package org.elasticsearch.common.time;
|
21 | 21 |
|
22 | 22 | import org.elasticsearch.common.Strings;
|
| 23 | +import org.elasticsearch.common.SuppressForbidden; |
23 | 24 |
|
24 |
| -import java.time.DateTimeException; |
25 | 25 | import java.time.DayOfWeek;
|
26 | 26 | import java.time.Instant;
|
27 | 27 | import java.time.LocalDate;
|
| 28 | +import java.time.LocalTime; |
| 29 | +import java.time.Year; |
28 | 30 | import java.time.ZoneId;
|
29 | 31 | import java.time.ZoneOffset;
|
30 | 32 | import java.time.ZonedDateTime;
|
@@ -1503,105 +1505,106 @@ static JavaDateFormatter merge(String pattern, List<DateFormatter> formatters) {
|
1503 | 1505 | dateTimeFormatters.toArray(new DateTimeFormatter[0]));
|
1504 | 1506 | }
|
1505 | 1507 |
|
1506 |
| - private static final ZonedDateTime EPOCH_ZONED_DATE_TIME = Instant.EPOCH.atZone(ZoneOffset.UTC); |
| 1508 | + private static final LocalDate LOCALDATE_EPOCH = LocalDate.of(1970, 1, 1); |
1507 | 1509 |
|
1508 |
| - public static ZonedDateTime toZonedDateTime(TemporalAccessor accessor) { |
1509 |
| - return toZonedDateTime(accessor, EPOCH_ZONED_DATE_TIME); |
1510 |
| - } |
1511 |
| - |
1512 |
| - public static ZonedDateTime toZonedDateTime(TemporalAccessor accessor, ZonedDateTime defaults) { |
1513 |
| - try { |
1514 |
| - return ZonedDateTime.from(accessor); |
1515 |
| - } catch (DateTimeException e ) { |
| 1510 | + /** |
| 1511 | + * Convert a temporal accessor to a zoned date time object - as performant as possible. |
| 1512 | + * The .from() methods from the JDK are throwing exceptions when for example ZonedDateTime.from(accessor) |
| 1513 | + * or Instant.from(accessor). This results in a huge performance penalty and should be prevented |
| 1514 | + * This method prevents exceptions by querying the accessor for certain capabilities |
| 1515 | + * and then act on it accordingly |
| 1516 | + * |
| 1517 | + * This action assumes that we can reliably fall back to some defaults if not all parts of a |
| 1518 | + * zoned date time are set |
| 1519 | + * |
| 1520 | + * - If a zoned date time is passed, it is returned |
| 1521 | + * - If no timezone is found, ZoneOffset.UTC is used |
| 1522 | + * - If we find a time and a date, converting to a ZonedDateTime is straight forward, |
| 1523 | + * no defaults will be applied |
| 1524 | + * - If an accessor only containing of seconds and nanos is found (like epoch_millis/second) |
| 1525 | + * an Instant is created out of that, that becomes a ZonedDateTime with a time zone |
| 1526 | + * - If no time is given, the start of the day is used |
| 1527 | + * - If no month of the year is found, the first day of the year is used |
| 1528 | + * - If an iso based weekyear is found, but not week is specified, the first monday |
| 1529 | + * of the new year is chosen (reataining BWC to joda time) |
| 1530 | + * - If an iso based weekyear is found and an iso based weekyear week, the start |
| 1531 | + * of the day is used |
| 1532 | + * |
| 1533 | + * @param accessor The accessor returned from a parser |
| 1534 | + * |
| 1535 | + * @return The converted zoned date time |
| 1536 | + */ |
| 1537 | + public static ZonedDateTime from(TemporalAccessor accessor) { |
| 1538 | + if (accessor instanceof ZonedDateTime) { |
| 1539 | + return (ZonedDateTime) accessor; |
1516 | 1540 | }
|
1517 | 1541 |
|
1518 |
| - ZonedDateTime result = defaults; |
1519 |
| - |
1520 |
| - // special case epoch seconds |
1521 |
| - if (accessor.isSupported(ChronoField.INSTANT_SECONDS)) { |
1522 |
| - result = result.with(ChronoField.INSTANT_SECONDS, accessor.getLong(ChronoField.INSTANT_SECONDS)); |
1523 |
| - if (accessor.isSupported(ChronoField.NANO_OF_SECOND)) { |
1524 |
| - result = result.with(ChronoField.NANO_OF_SECOND, accessor.getLong(ChronoField.NANO_OF_SECOND)); |
1525 |
| - } |
1526 |
| - return result; |
| 1542 | + ZoneId zoneId = accessor.query(TemporalQueries.zone()); |
| 1543 | + if (zoneId == null) { |
| 1544 | + zoneId = ZoneOffset.UTC; |
1527 | 1545 | }
|
1528 | 1546 |
|
1529 |
| - // try to set current year |
1530 |
| - if (accessor.isSupported(ChronoField.YEAR)) { |
1531 |
| - result = result.with(ChronoField.YEAR, accessor.getLong(ChronoField.YEAR)); |
1532 |
| - } else if (accessor.isSupported(ChronoField.YEAR_OF_ERA)) { |
1533 |
| - result = result.with(ChronoField.YEAR_OF_ERA, accessor.getLong(ChronoField.YEAR_OF_ERA)); |
| 1547 | + LocalDate localDate = accessor.query(TemporalQueries.localDate()); |
| 1548 | + LocalTime localTime = accessor.query(TemporalQueries.localTime()); |
| 1549 | + boolean isLocalDateSet = localDate != null; |
| 1550 | + boolean isLocalTimeSet = localTime != null; |
| 1551 | + |
| 1552 | + // the first two cases are the most common, so this allows us to exit early when parsing dates |
| 1553 | + if (isLocalDateSet && isLocalTimeSet) { |
| 1554 | + return of(localDate, localTime, zoneId); |
| 1555 | + } else if (accessor.isSupported(ChronoField.INSTANT_SECONDS) && accessor.isSupported(NANO_OF_SECOND)) { |
| 1556 | + return Instant.from(accessor).atZone(zoneId); |
| 1557 | + } else if (isLocalDateSet) { |
| 1558 | + return localDate.atStartOfDay(zoneId); |
| 1559 | + } else if (isLocalTimeSet) { |
| 1560 | + return of(getLocaldate(accessor), localTime, zoneId); |
| 1561 | + } else if (accessor.isSupported(ChronoField.YEAR)) { |
| 1562 | + if (accessor.isSupported(MONTH_OF_YEAR)) { |
| 1563 | + return getFirstOfMonth(accessor).atStartOfDay(zoneId); |
| 1564 | + } else { |
| 1565 | + return Year.of(accessor.get(ChronoField.YEAR)).atDay(1).atStartOfDay(zoneId); |
| 1566 | + } |
| 1567 | + } else if (accessor.isSupported(MONTH_OF_YEAR)) { |
| 1568 | + // missing year, falling back to the epoch and then filling |
| 1569 | + return getLocaldate(accessor).atStartOfDay(zoneId); |
1534 | 1570 | } else if (accessor.isSupported(WeekFields.ISO.weekBasedYear())) {
|
1535 | 1571 | if (accessor.isSupported(WeekFields.ISO.weekOfWeekBasedYear())) {
|
1536 |
| - return LocalDate.from(result) |
1537 |
| - .with(WeekFields.ISO.weekBasedYear(), accessor.getLong(WeekFields.ISO.weekBasedYear())) |
1538 |
| - .withDayOfMonth(1) // makes this compatible with joda |
| 1572 | + return Year.of(accessor.get(WeekFields.ISO.weekBasedYear())) |
| 1573 | + .atDay(1) |
1539 | 1574 | .with(WeekFields.ISO.weekOfWeekBasedYear(), accessor.getLong(WeekFields.ISO.weekOfWeekBasedYear()))
|
1540 |
| - .atStartOfDay(ZoneOffset.UTC); |
| 1575 | + .atStartOfDay(zoneId); |
1541 | 1576 | } else {
|
1542 |
| - return LocalDate.from(result) |
1543 |
| - .with(WeekFields.ISO.weekBasedYear(), accessor.getLong(WeekFields.ISO.weekBasedYear())) |
1544 |
| - // this exists solely to be BWC compatible with joda |
1545 |
| -// .with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY)) |
| 1577 | + return Year.of(accessor.get(WeekFields.ISO.weekBasedYear())) |
| 1578 | + .atDay(1) |
1546 | 1579 | .with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY))
|
1547 |
| - .atStartOfDay(defaults.getZone()); |
1548 |
| -// return result.withHour(0).withMinute(0).withSecond(0) |
1549 |
| -// .with(WeekFields.ISO.weekBasedYear(), 0) |
1550 |
| -// .with(WeekFields.ISO.weekBasedYear(), accessor.getLong(WeekFields.ISO.weekBasedYear())); |
1551 |
| -// return ((ZonedDateTime) tmp).with(WeekFields.ISO.weekOfWeekBasedYear(), 1); |
| 1580 | + .atStartOfDay(zoneId); |
1552 | 1581 | }
|
1553 |
| - } else if (accessor.isSupported(IsoFields.WEEK_BASED_YEAR)) { |
1554 |
| - // special case weekbased year |
1555 |
| - result = result.with(IsoFields.WEEK_BASED_YEAR, accessor.getLong(IsoFields.WEEK_BASED_YEAR)); |
1556 |
| - if (accessor.isSupported(IsoFields.WEEK_OF_WEEK_BASED_YEAR)) { |
1557 |
| - result = result.with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, accessor.getLong(IsoFields.WEEK_OF_WEEK_BASED_YEAR)); |
1558 |
| - } |
1559 |
| - return result; |
1560 |
| - } |
1561 |
| - |
1562 |
| - // month |
1563 |
| - if (accessor.isSupported(ChronoField.MONTH_OF_YEAR)) { |
1564 |
| - result = result.with(ChronoField.MONTH_OF_YEAR, accessor.getLong(ChronoField.MONTH_OF_YEAR)); |
1565 | 1582 | }
|
1566 | 1583 |
|
1567 |
| - // day of month |
1568 |
| - if (accessor.isSupported(ChronoField.DAY_OF_MONTH)) { |
1569 |
| - result = result.with(ChronoField.DAY_OF_MONTH, accessor.getLong(ChronoField.DAY_OF_MONTH)); |
1570 |
| - } |
1571 |
| - |
1572 |
| - // hour |
1573 |
| - if (accessor.isSupported(ChronoField.HOUR_OF_DAY)) { |
1574 |
| - result = result.with(ChronoField.HOUR_OF_DAY, accessor.getLong(ChronoField.HOUR_OF_DAY)); |
1575 |
| - } |
1576 |
| - |
1577 |
| - // minute |
1578 |
| - if (accessor.isSupported(ChronoField.MINUTE_OF_HOUR)) { |
1579 |
| - result = result.with(ChronoField.MINUTE_OF_HOUR, accessor.getLong(ChronoField.MINUTE_OF_HOUR)); |
1580 |
| - } |
1581 |
| - |
1582 |
| - // second |
1583 |
| - if (accessor.isSupported(ChronoField.SECOND_OF_MINUTE)) { |
1584 |
| - result = result.with(ChronoField.SECOND_OF_MINUTE, accessor.getLong(ChronoField.SECOND_OF_MINUTE)); |
1585 |
| - } |
1586 |
| - |
1587 |
| - if (accessor.isSupported(ChronoField.OFFSET_SECONDS)) { |
1588 |
| - result = result.withZoneSameLocal(ZoneOffset.ofTotalSeconds(accessor.get(ChronoField.OFFSET_SECONDS))); |
1589 |
| - } |
| 1584 | + // we should not reach this piece of code, everything being parsed we should be able to |
| 1585 | + // convert to a zoned date time! If not, we have to extend the above methods |
| 1586 | + throw new IllegalArgumentException("temporal accessor [" + accessor + "] cannot be converted to zoned date time"); |
| 1587 | + } |
1590 | 1588 |
|
1591 |
| - // millis |
1592 |
| - if (accessor.isSupported(ChronoField.MILLI_OF_SECOND)) { |
1593 |
| - result = result.with(ChronoField.MILLI_OF_SECOND, accessor.getLong(ChronoField.MILLI_OF_SECOND)); |
| 1589 | + private static LocalDate getLocaldate(TemporalAccessor accessor) { |
| 1590 | + if (accessor.isSupported(MONTH_OF_YEAR)) { |
| 1591 | + if (accessor.isSupported(DAY_OF_MONTH)) { |
| 1592 | + return LocalDate.of(1970, accessor.get(MONTH_OF_YEAR), accessor.get(DAY_OF_MONTH)); |
| 1593 | + } else { |
| 1594 | + return LocalDate.of(1970, accessor.get(MONTH_OF_YEAR), 1); |
| 1595 | + } |
1594 | 1596 | }
|
1595 | 1597 |
|
1596 |
| - if (accessor.isSupported(ChronoField.NANO_OF_SECOND)) { |
1597 |
| - result = result.with(ChronoField.NANO_OF_SECOND, accessor.getLong(ChronoField.NANO_OF_SECOND)); |
1598 |
| - } |
| 1598 | + return LOCALDATE_EPOCH; |
| 1599 | + } |
1599 | 1600 |
|
1600 |
| - ZoneId zoneOffset = accessor.query(TemporalQueries.zone()); |
1601 |
| - if (zoneOffset != null) { |
1602 |
| - result = result.withZoneSameLocal(zoneOffset); |
1603 |
| - } |
| 1601 | + @SuppressForbidden(reason = "ZonedDateTime.of is fine here") |
| 1602 | + private static ZonedDateTime of(LocalDate localDate, LocalTime localTime, ZoneId zoneId) { |
| 1603 | + return ZonedDateTime.of(localDate, localTime, zoneId); |
| 1604 | + } |
1604 | 1605 |
|
1605 |
| - return result; |
| 1606 | + @SuppressForbidden(reason = "LocalDate.of is fine here") |
| 1607 | + private static LocalDate getFirstOfMonth(TemporalAccessor accessor) { |
| 1608 | + return LocalDate.of(accessor.get(ChronoField.YEAR), accessor.get(MONTH_OF_YEAR), 1); |
1606 | 1609 | }
|
1607 | 1610 | }
|
0 commit comments