diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java index 9baedf6b65e33..f60866383107a 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java @@ -30,7 +30,6 @@ import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.time.DateFormatters; import org.elasticsearch.common.time.DateMathParser; -import org.elasticsearch.common.time.JavaDateMathParser; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexNotFoundException; @@ -921,7 +920,7 @@ String resolveExpression(String expression, final Context context) { dateFormatter = DateFormatters.forPattern(dateFormatterPattern); } DateFormatter formatter = dateFormatter.withZone(timeZone); - DateMathParser dateMathParser = new JavaDateMathParser(formatter); + DateMathParser dateMathParser = formatter.toDateMathParser(); long millis = dateMathParser.parse(mathExpression, context::getStartTime, false, timeZone); String time = formatter.format(Instant.ofEpochMilli(millis)); diff --git a/server/src/main/java/org/elasticsearch/common/time/DateFormatter.java b/server/src/main/java/org/elasticsearch/common/time/DateFormatter.java index b952f1d69bcd6..4c9487fe3bd6b 100644 --- a/server/src/main/java/org/elasticsearch/common/time/DateFormatter.java +++ b/server/src/main/java/org/elasticsearch/common/time/DateFormatter.java @@ -19,13 +19,13 @@ package org.elasticsearch.common.time; +import org.elasticsearch.ElasticsearchParseException; + import java.time.ZoneId; import java.time.format.DateTimeParseException; import java.time.temporal.TemporalAccessor; -import java.time.temporal.TemporalField; import java.util.Arrays; import java.util.Locale; -import java.util.Map; import java.util.stream.Collectors; public interface DateFormatter { @@ -86,13 +86,9 @@ public interface DateFormatter { ZoneId getZone(); /** - * Configure a formatter using default fields for a TemporalAccessor that should be used in case - * the supplied date is not having all of those fields - * - * @param fields A Map<TemporalField, Long> of fields to be used as fallbacks - * @return A new date formatter instance, that will use those fields during parsing + * Return a {@link DateMathParser} built from this formatter. */ - DateFormatter parseDefaulting(Map fields); + DateMathParser toDateMathParser(); /** * Merge several date formatters into a single one. Useful if you need to have several formatters with @@ -102,7 +98,7 @@ public interface DateFormatter { * @param formatters The list of date formatters to be merged together * @return The new date formtter containing the specified date formatters */ - static DateFormatter merge(DateFormatter ... formatters) { + static DateFormatter merge(DateFormatter... formatters) { return new MergedDateFormatter(formatters); } @@ -110,10 +106,12 @@ class MergedDateFormatter implements DateFormatter { private final String format; private final DateFormatter[] formatters; + private final DateMathParser[] dateMathParsers; - MergedDateFormatter(DateFormatter ... formatters) { + MergedDateFormatter(DateFormatter... formatters) { this.formatters = formatters; this.format = Arrays.stream(formatters).map(DateFormatter::pattern).collect(Collectors.joining("||")); + this.dateMathParsers = Arrays.stream(formatters).map(DateFormatter::toDateMathParser).toArray(DateMathParser[]::new); } @Override @@ -164,8 +162,22 @@ public ZoneId getZone() { } @Override - public DateFormatter parseDefaulting(Map fields) { - return new MergedDateFormatter(Arrays.stream(formatters).map(f -> f.parseDefaulting(fields)).toArray(DateFormatter[]::new)); + public DateMathParser toDateMathParser() { + return (text, now, roundUp, tz) -> { + ElasticsearchParseException failure = null; + for (DateMathParser parser : dateMathParsers) { + try { + return parser.parse(text, now, roundUp, tz); + } catch (ElasticsearchParseException e) { + if (failure == null) { + failure = e; + } else { + failure.addSuppressed(e); + } + } + } + throw failure; + }; } } } diff --git a/server/src/main/java/org/elasticsearch/common/time/EpochMillisDateFormatter.java b/server/src/main/java/org/elasticsearch/common/time/EpochMillisDateFormatter.java index 4fce63510029d..fc7c44c71424c 100644 --- a/server/src/main/java/org/elasticsearch/common/time/EpochMillisDateFormatter.java +++ b/server/src/main/java/org/elasticsearch/common/time/EpochMillisDateFormatter.java @@ -25,9 +25,7 @@ import java.time.ZoneOffset; import java.time.format.DateTimeParseException; import java.time.temporal.TemporalAccessor; -import java.time.temporal.TemporalField; import java.util.Locale; -import java.util.Map; import java.util.regex.Pattern; /** @@ -42,7 +40,8 @@ class EpochMillisDateFormatter implements DateFormatter { private static final Pattern SPLIT_BY_DOT_PATTERN = Pattern.compile("\\."); - static DateFormatter INSTANCE = new EpochMillisDateFormatter(); + static final DateFormatter INSTANCE = new EpochMillisDateFormatter(); + static final DateMathParser DATE_MATH_INSTANCE = new JavaDateMathParser(INSTANCE, INSTANCE); private EpochMillisDateFormatter() { } @@ -103,11 +102,6 @@ public String pattern() { return "epoch_millis"; } - @Override - public DateFormatter parseDefaulting(Map fields) { - return this; - } - @Override public Locale getLocale() { return Locale.ROOT; @@ -117,4 +111,9 @@ public Locale getLocale() { public ZoneId getZone() { return ZoneOffset.UTC; } + + @Override + public DateMathParser toDateMathParser() { + return DATE_MATH_INSTANCE; + } } diff --git a/server/src/main/java/org/elasticsearch/common/time/EpochSecondsDateFormatter.java b/server/src/main/java/org/elasticsearch/common/time/EpochSecondsDateFormatter.java index 218542f817be6..8215355a67821 100644 --- a/server/src/main/java/org/elasticsearch/common/time/EpochSecondsDateFormatter.java +++ b/server/src/main/java/org/elasticsearch/common/time/EpochSecondsDateFormatter.java @@ -25,14 +25,13 @@ import java.time.ZoneOffset; import java.time.format.DateTimeParseException; import java.time.temporal.TemporalAccessor; -import java.time.temporal.TemporalField; import java.util.Locale; -import java.util.Map; import java.util.regex.Pattern; public class EpochSecondsDateFormatter implements DateFormatter { public static DateFormatter INSTANCE = new EpochSecondsDateFormatter(); + static final DateMathParser DATE_MATH_INSTANCE = new JavaDateMathParser(INSTANCE, INSTANCE); private static final Pattern SPLIT_BY_DOT_PATTERN = Pattern.compile("\\."); private EpochSecondsDateFormatter() {} @@ -91,6 +90,11 @@ public ZoneId getZone() { return ZoneOffset.UTC; } + @Override + public DateMathParser toDateMathParser() { + return DATE_MATH_INSTANCE; + } + @Override public DateFormatter withZone(ZoneId zoneId) { if (zoneId.equals(ZoneOffset.UTC) == false) { @@ -106,9 +110,4 @@ public DateFormatter withLocale(Locale locale) { } return this; } - - @Override - public DateFormatter parseDefaulting(Map fields) { - return this; - } } diff --git a/server/src/main/java/org/elasticsearch/common/time/JavaDateFormatter.java b/server/src/main/java/org/elasticsearch/common/time/JavaDateFormatter.java index 75cd82b51e85a..440bdb3f80b7c 100644 --- a/server/src/main/java/org/elasticsearch/common/time/JavaDateFormatter.java +++ b/server/src/main/java/org/elasticsearch/common/time/JavaDateFormatter.java @@ -23,15 +23,28 @@ import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoField; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalField; import java.util.Arrays; +import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Objects; class JavaDateFormatter implements DateFormatter { + // base fields which should be used for default parsing, when we round up for date math + private static final Map ROUND_UP_BASE_FIELDS = new HashMap<>(6); + { + ROUND_UP_BASE_FIELDS.put(ChronoField.MONTH_OF_YEAR, 1L); + ROUND_UP_BASE_FIELDS.put(ChronoField.DAY_OF_MONTH, 1L); + ROUND_UP_BASE_FIELDS.put(ChronoField.HOUR_OF_DAY, 23L); + ROUND_UP_BASE_FIELDS.put(ChronoField.MINUTE_OF_HOUR, 59L); + ROUND_UP_BASE_FIELDS.put(ChronoField.SECOND_OF_MINUTE, 59L); + ROUND_UP_BASE_FIELDS.put(ChronoField.MILLI_OF_SECOND, 999L); + } + private final String format; private final DateTimeFormatter printer; private final DateTimeFormatter[] parsers; @@ -116,8 +129,7 @@ public String pattern() { return format; } - @Override - public DateFormatter parseDefaulting(Map fields) { + JavaDateFormatter parseDefaulting(Map fields) { final DateTimeFormatterBuilder parseDefaultingBuilder = new DateTimeFormatterBuilder().append(printer); fields.forEach(parseDefaultingBuilder::parseDefaulting); if (parsers.length == 1 && parsers[0].equals(printer)) { @@ -143,6 +155,11 @@ public ZoneId getZone() { return this.printer.getZone(); } + @Override + public DateMathParser toDateMathParser() { + return new JavaDateMathParser(this, this.parseDefaulting(ROUND_UP_BASE_FIELDS)); + } + @Override public int hashCode() { return Objects.hash(getLocale(), printer.getZone(), format); diff --git a/server/src/main/java/org/elasticsearch/common/time/JavaDateMathParser.java b/server/src/main/java/org/elasticsearch/common/time/JavaDateMathParser.java index c3a59f521904b..2d3fb7e176e89 100644 --- a/server/src/main/java/org/elasticsearch/common/time/JavaDateMathParser.java +++ b/server/src/main/java/org/elasticsearch/common/time/JavaDateMathParser.java @@ -31,10 +31,7 @@ import java.time.temporal.ChronoField; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalAdjusters; -import java.time.temporal.TemporalField; import java.time.temporal.TemporalQueries; -import java.util.HashMap; -import java.util.Map; import java.util.Objects; import java.util.function.LongSupplier; @@ -47,24 +44,15 @@ */ public class JavaDateMathParser implements DateMathParser { - // base fields which should be used for default parsing, when we round up - private static final Map ROUND_UP_BASE_FIELDS = new HashMap<>(6); - { - ROUND_UP_BASE_FIELDS.put(ChronoField.MONTH_OF_YEAR, 1L); - ROUND_UP_BASE_FIELDS.put(ChronoField.DAY_OF_MONTH, 1L); - ROUND_UP_BASE_FIELDS.put(ChronoField.HOUR_OF_DAY, 23L); - ROUND_UP_BASE_FIELDS.put(ChronoField.MINUTE_OF_HOUR, 59L); - ROUND_UP_BASE_FIELDS.put(ChronoField.SECOND_OF_MINUTE, 59L); - ROUND_UP_BASE_FIELDS.put(ChronoField.MILLI_OF_SECOND, 999L); - } + private final DateFormatter formatter; private final DateFormatter roundUpFormatter; - public JavaDateMathParser(DateFormatter formatter) { + public JavaDateMathParser(DateFormatter formatter, DateFormatter roundUpFormatter) { Objects.requireNonNull(formatter); this.formatter = formatter; - this.roundUpFormatter = formatter.parseDefaulting(ROUND_UP_BASE_FIELDS); + this.roundUpFormatter = roundUpFormatter; } @Override diff --git a/server/src/test/java/org/elasticsearch/common/time/JavaDateMathParserTests.java b/server/src/test/java/org/elasticsearch/common/time/JavaDateMathParserTests.java index a543af0445db1..ce625c33fea8d 100644 --- a/server/src/test/java/org/elasticsearch/common/time/JavaDateMathParserTests.java +++ b/server/src/test/java/org/elasticsearch/common/time/JavaDateMathParserTests.java @@ -36,7 +36,7 @@ public class JavaDateMathParserTests extends ESTestCase { private final DateFormatter formatter = DateFormatters.forPattern("dateOptionalTime||epoch_millis"); - private final JavaDateMathParser parser = new JavaDateMathParser(formatter); + private final DateMathParser parser = formatter.toDateMathParser(); public void testBasicDates() { assertDateMathEquals("2014", "2014-01-01T00:00:00.000"); @@ -139,7 +139,7 @@ public void testNow() { public void testRoundingPreservesEpochAsBaseDate() { // If a user only specifies times, then the date needs to always be 1970-01-01 regardless of rounding DateFormatter formatter = DateFormatters.forPattern("HH:mm:ss"); - JavaDateMathParser parser = new JavaDateMathParser(formatter); + DateMathParser parser = formatter.toDateMathParser(); ZonedDateTime zonedDateTime = DateFormatters.toZonedDateTime(formatter.parse("04:52:20")); assertThat(zonedDateTime.getYear(), is(1970)); long millisStart = zonedDateTime.toInstant().toEpochMilli(); @@ -165,7 +165,7 @@ public void testImplicitRounding() { // implicit rounding with explicit timezone in the date format DateFormatter formatter = DateFormatters.forPattern("yyyy-MM-ddXXX"); - JavaDateMathParser parser = new JavaDateMathParser(formatter); + DateMathParser parser = formatter.toDateMathParser(); long time = parser.parse("2011-10-09+01:00", () -> 0, false, (ZoneId) null); assertEquals(this.parser.parse("2011-10-09T00:00:00.000+01:00", () -> 0), time); time = parser.parse("2011-10-09+01:00", () -> 0, true, (ZoneId) null); @@ -239,7 +239,7 @@ public void testTimestamps() { assertDateMathEquals("1418248078000||/m", "2014-12-10T21:47:00.000"); // also check other time units - JavaDateMathParser parser = new JavaDateMathParser(DateFormatters.forPattern("epoch_second||dateOptionalTime")); + DateMathParser parser = DateFormatters.forPattern("epoch_second||dateOptionalTime").toDateMathParser(); long datetime = parser.parse("1418248078", () -> 0); assertDateEquals(datetime, "1418248078", "2014-12-10T21:47:58.000");