浏览代码

Core: Fix epoch millis java time formatter (#33302)

The existing implemention could not deal with negative numbers as well
as +- 999 milliseconds around the epoch.

This commit uses Instant.ofEpochMilli() and parses the input to
a number instead of using a date formatter.
Alexander Reelsen 7 年之前
父节点
当前提交
246a7df8c2

+ 40 - 2
server/src/main/java/org/elasticsearch/common/time/DateFormatters.java

@@ -25,10 +25,12 @@ import java.time.DateTimeException;
 import java.time.DayOfWeek;
 import java.time.Instant;
 import java.time.LocalDate;
+import java.time.ZoneId;
 import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeFormatterBuilder;
+import java.time.format.DateTimeParseException;
 import java.time.format.ResolverStyle;
 import java.time.format.SignStyle;
 import java.time.temporal.ChronoField;
@@ -879,11 +881,47 @@ public class DateFormatters {
 
     /*
      * Returns a formatter for parsing the milliseconds since the epoch
+     * This one needs a custom implementation, because the standard date formatter can not parse negative values
+     * or anything +- 999 milliseconds around the epoch
+     *
+     * This implementation just resorts to parsing the input directly to an Instant by trying to parse a number.
      */
-    private static final CompoundDateTimeFormatter EPOCH_MILLIS = new CompoundDateTimeFormatter(new DateTimeFormatterBuilder()
+    private static final DateTimeFormatter EPOCH_MILLIS_FORMATTER = new DateTimeFormatterBuilder()
         .appendValue(ChronoField.INSTANT_SECONDS, 1, 19, SignStyle.NEVER)
         .appendValue(ChronoField.MILLI_OF_SECOND, 3)
-        .toFormatter(Locale.ROOT));
+        .toFormatter(Locale.ROOT);
+
+    private static final class EpochDateTimeFormatter extends CompoundDateTimeFormatter {
+
+        private EpochDateTimeFormatter() {
+            super(EPOCH_MILLIS_FORMATTER);
+        }
+
+        private EpochDateTimeFormatter(ZoneId zoneId) {
+            super(EPOCH_MILLIS_FORMATTER.withZone(zoneId));
+        }
+
+        @Override
+        public TemporalAccessor parse(String input) {
+            try {
+                return Instant.ofEpochMilli(Long.valueOf(input)).atZone(ZoneOffset.UTC);
+            } catch (NumberFormatException e) {
+                throw new DateTimeParseException("invalid number", input, 0, e);
+            }
+        }
+
+        @Override
+        public CompoundDateTimeFormatter withZone(ZoneId zoneId) {
+            return new EpochDateTimeFormatter(zoneId);
+        }
+
+        @Override
+        public String format(TemporalAccessor accessor) {
+            return String.valueOf(Instant.from(accessor).toEpochMilli());
+        }
+    }
+
+    private static final CompoundDateTimeFormatter EPOCH_MILLIS = new EpochDateTimeFormatter();
 
     /*
      * Returns a formatter that combines a full date and two digit hour of

+ 8 - 0
server/src/test/java/org/elasticsearch/common/joda/JavaJodaTimeDuellingTests.java

@@ -71,7 +71,15 @@ public class JavaJodaTimeDuellingTests extends ESTestCase {
 
     public void testDuellingFormatsValidParsing() {
         assertSameDate("1522332219", "epoch_second");
+        assertSameDate("0", "epoch_second");
+        assertSameDate("1", "epoch_second");
+        assertSameDate("-1", "epoch_second");
+        assertSameDate("-1522332219", "epoch_second");
         assertSameDate("1522332219321", "epoch_millis");
+        assertSameDate("0", "epoch_millis");
+        assertSameDate("1", "epoch_millis");
+        assertSameDate("-1", "epoch_millis");
+        assertSameDate("-1522332219321", "epoch_millis");
 
         assertSameDate("20181126", "basic_date");
         assertSameDate("20181126T121212.123Z", "basic_date_time");

+ 73 - 0
server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java

@@ -0,0 +1,73 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.common.time;
+
+import org.elasticsearch.test.ESTestCase;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.TemporalAccessor;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+
+public class DateFormattersTests extends ESTestCase {
+
+    // the epoch milli parser is a bit special, as it does not use date formatter, see comments in DateFormatters
+    public void testEpochMilliParser() {
+        CompoundDateTimeFormatter formatter = DateFormatters.forPattern("epoch_millis");
+
+        DateTimeParseException e = expectThrows(DateTimeParseException.class, () -> formatter.parse("invalid"));
+        assertThat(e.getMessage(), containsString("invalid number"));
+
+        // different zone, should still yield the same output, as epoch is time zoned independent
+        ZoneId zoneId = randomZone();
+        CompoundDateTimeFormatter zonedFormatter = formatter.withZone(zoneId);
+        assertThat(zonedFormatter.printer.getZone(), is(zoneId));
+
+        // test with negative and non negative values
+        assertThatSameDateTime(formatter, zonedFormatter, randomNonNegativeLong() * -1);
+        assertThatSameDateTime(formatter, zonedFormatter, randomNonNegativeLong());
+        assertThatSameDateTime(formatter, zonedFormatter, 0);
+        assertThatSameDateTime(formatter, zonedFormatter, -1);
+        assertThatSameDateTime(formatter, zonedFormatter, 1);
+
+        // format() output should be equal as well
+        assertSameFormat(formatter, randomNonNegativeLong() * -1);
+        assertSameFormat(formatter, randomNonNegativeLong());
+        assertSameFormat(formatter, 0);
+        assertSameFormat(formatter, -1);
+        assertSameFormat(formatter, 1);
+    }
+
+    private void assertThatSameDateTime(CompoundDateTimeFormatter formatter, CompoundDateTimeFormatter zonedFormatter, long millis) {
+        String millisAsString = String.valueOf(millis);
+        ZonedDateTime formatterZonedDateTime = DateFormatters.toZonedDateTime(formatter.parse(millisAsString));
+        ZonedDateTime zonedFormatterZonedDateTime = DateFormatters.toZonedDateTime(zonedFormatter.parse(millisAsString));
+        assertThat(formatterZonedDateTime.toInstant().toEpochMilli(), is(zonedFormatterZonedDateTime.toInstant().toEpochMilli()));
+    }
+
+    private void assertSameFormat(CompoundDateTimeFormatter formatter, long millis) {
+        String millisAsString = String.valueOf(millis);
+        TemporalAccessor accessor = formatter.parse(millisAsString);
+        assertThat(millisAsString, is(formatter.format(accessor)));
+    }
+}