|
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