Browse Source

Core: Abstract DateMathParser in an interface (#33905)

This commits creates a DateMathParser interface, which is already
implemented for both joda and java time. While currently the java time
DateMathParser is not used, this change will allow a followup which will
create a DateMathParser from a DateFormatter, so the caller does not
need to know the internals of the DateFormatter they have.
Ryan Ernst 7 years ago
parent
commit
7800b4fa91
26 changed files with 476 additions and 293 deletions
  1. 5 3
      server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java
  2. 5 0
      server/src/main/java/org/elasticsearch/common/joda/FormatDateTimeFormatter.java
  3. 8 7
      server/src/main/java/org/elasticsearch/common/joda/JodaDateMathParser.java
  4. 17 210
      server/src/main/java/org/elasticsearch/common/time/DateMathParser.java
  5. 70 0
      server/src/main/java/org/elasticsearch/common/time/DateUtils.java
  6. 240 0
      server/src/main/java/org/elasticsearch/common/time/JavaDateMathParser.java
  7. 10 9
      server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java
  8. 1 1
      server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java
  9. 9 6
      server/src/main/java/org/elasticsearch/index/mapper/RangeFieldMapper.java
  10. 2 2
      server/src/main/java/org/elasticsearch/index/mapper/SimpleMappedFieldType.java
  11. 2 2
      server/src/main/java/org/elasticsearch/index/query/RangeQueryBuilder.java
  12. 5 3
      server/src/main/java/org/elasticsearch/search/DocValueFormat.java
  13. 2 2
      server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramAggregationBuilder.java
  14. 15 15
      server/src/test/java/org/elasticsearch/common/joda/JodaDateMathParserTests.java
  15. 54 0
      server/src/test/java/org/elasticsearch/common/time/DateUtilsTests.java
  16. 12 12
      server/src/test/java/org/elasticsearch/common/time/JavaDateMathParserTests.java
  17. 3 3
      server/src/test/java/org/elasticsearch/index/mapper/DateFieldTypeTests.java
  18. 1 1
      server/src/test/java/org/elasticsearch/index/mapper/RangeFieldQueryStringQueryBuilderTests.java
  19. 2 2
      server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateHistogramIT.java
  20. 1 1
      test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java
  21. 2 3
      x-pack/license-tools/src/test/java/org/elasticsearch/license/licensor/TestUtils.java
  22. 2 2
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetOverallBucketsAction.java
  23. 2 2
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StartDatafeedAction.java
  24. 2 2
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/WatcherDateTimeUtils.java
  25. 2 2
      x-pack/plugin/core/src/test/java/org/elasticsearch/license/TestUtils.java
  26. 2 3
      x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupIndexerIndexingTests.java

+ 5 - 3
server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java

@@ -27,10 +27,11 @@ import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.collect.Tuple;
 import org.elasticsearch.common.component.AbstractComponent;
-import org.elasticsearch.common.joda.DateMathParser;
 import org.elasticsearch.common.joda.FormatDateTimeFormatter;
 import org.elasticsearch.common.regex.Regex;
 import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.time.DateMathParser;
+import org.elasticsearch.common.time.DateUtils;
 import org.elasticsearch.common.util.set.Sets;
 import org.elasticsearch.index.Index;
 import org.elasticsearch.index.IndexNotFoundException;
@@ -923,8 +924,9 @@ public class IndexNameExpressionResolver extends AbstractComponent {
                                 }
                                 DateTimeFormatter parser = dateFormatter.withZone(timeZone);
                                 FormatDateTimeFormatter formatter = new FormatDateTimeFormatter(dateFormatterPattern, parser, Locale.ROOT);
-                                DateMathParser dateMathParser = new DateMathParser(formatter);
-                            long millis = dateMathParser.parse(mathExpression, context::getStartTime, false, timeZone);
+                                DateMathParser dateMathParser = formatter.toDateMathParser();
+                                long millis = dateMathParser.parse(mathExpression, context::getStartTime, false,
+                                    DateUtils.dateTimeZoneToZoneId(timeZone));
 
                                 String time = formatter.printer().print(millis);
                                 beforePlaceHolderSb.append(time);

+ 5 - 0
server/src/main/java/org/elasticsearch/common/joda/FormatDateTimeFormatter.java

@@ -19,6 +19,7 @@
 
 package org.elasticsearch.common.joda;
 
+import org.elasticsearch.common.time.DateMathParser;
 import org.joda.time.format.DateTimeFormatter;
 
 import java.util.Locale;
@@ -64,4 +65,8 @@ public class FormatDateTimeFormatter {
     public Locale locale() {
         return locale;
     }
+
+    public DateMathParser toDateMathParser() {
+        return new JodaDateMathParser(this);
+    }
 }

+ 8 - 7
server/src/main/java/org/elasticsearch/common/joda/DateMathParser.java → server/src/main/java/org/elasticsearch/common/joda/JodaDateMathParser.java

@@ -20,10 +20,13 @@
 package org.elasticsearch.common.joda;
 
 import org.elasticsearch.ElasticsearchParseException;
+import org.elasticsearch.common.time.DateMathParser;
+import org.elasticsearch.common.time.DateUtils;
 import org.joda.time.DateTimeZone;
 import org.joda.time.MutableDateTime;
 import org.joda.time.format.DateTimeFormatter;
 
+import java.time.ZoneId;
 import java.util.Objects;
 import java.util.function.LongSupplier;
 
@@ -34,23 +37,21 @@ import java.util.function.LongSupplier;
  * is appended to a datetime with the following syntax:
  * <code>||[+-/](\d+)?[yMwdhHms]</code>.
  */
-public class DateMathParser {
+public class JodaDateMathParser implements DateMathParser {
 
     private final FormatDateTimeFormatter dateTimeFormatter;
 
-    public DateMathParser(FormatDateTimeFormatter dateTimeFormatter) {
+    public JodaDateMathParser(FormatDateTimeFormatter dateTimeFormatter) {
         Objects.requireNonNull(dateTimeFormatter);
         this.dateTimeFormatter = dateTimeFormatter;
     }
 
-    public long parse(String text, LongSupplier now) {
-        return parse(text, now, false, null);
-    }
-
     // Note: we take a callable here for the timestamp in order to be able to figure out
     // if it has been used. For instance, the request cache does not cache requests that make
     // use of `now`.
-    public long parse(String text, LongSupplier now, boolean roundUp, DateTimeZone timeZone) {
+    @Override
+    public long parse(String text, LongSupplier now, boolean roundUp, ZoneId tz) {
+        final DateTimeZone timeZone = tz == null ? null : DateUtils.zoneIdToDateTimeZone(tz);
         long time;
         String mathString;
         if (text.startsWith("now")) {

+ 17 - 210
server/src/main/java/org/elasticsearch/common/time/DateMathParser.java

@@ -19,56 +19,31 @@
 
 package org.elasticsearch.common.time;
 
-import org.elasticsearch.ElasticsearchParseException;
+import org.joda.time.DateTimeZone;
 
-import java.time.DateTimeException;
-import java.time.DayOfWeek;
-import java.time.Instant;
-import java.time.LocalTime;
 import java.time.ZoneId;
-import java.time.ZoneOffset;
-import java.time.ZonedDateTime;
-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;
 
 /**
- * A parser for date/time formatted text with optional date math.
- *
- * The format of the datetime is configurable, and unix timestamps can also be used. Datemath
- * is appended to a datetime with the following syntax:
- * <code>||[+-/](\d+)?[yMwdhHms]</code>.
+ * An abstraction over date math parsing to allow different implementation for joda and java time.
  */
-public class DateMathParser {
+public interface DateMathParser {
 
-    // base fields which should be used for default parsing, when we round up
-    private static final Map<TemporalField, Long> 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);
+    /**
+     * Parse a date math expression without timzeone info and rounding down.
+     */
+    default long parse(String text, LongSupplier now) {
+        return parse(text, now, false, (ZoneId) null);
     }
 
-    private final DateFormatter formatter;
-    private final DateFormatter roundUpFormatter;
+    // Note: we take a callable here for the timestamp in order to be able to figure out
+    // if it has been used. For instance, the request cache does not cache requests that make
+    // use of `now`.
 
-    public DateMathParser(DateFormatter formatter) {
-        Objects.requireNonNull(formatter);
-        this.formatter = formatter;
-        this.roundUpFormatter = formatter.parseDefaulting(ROUND_UP_BASE_FIELDS);
-    }
-
-    public long parse(String text, LongSupplier now) {
-        return parse(text, now, false, null);
+    // exists for backcompat, do not use!
+    @Deprecated
+    default long parse(String text, LongSupplier now, boolean roundUp, DateTimeZone tz) {
+        return parse(text, now, roundUp, tz == null ? null : ZoneId.of(tz.getID()));
     }
 
     /**
@@ -92,176 +67,8 @@ public class DateMathParser {
      * @param text      the input
      * @param now       a supplier to retrieve the current date in milliseconds, if needed for additions
      * @param roundUp   should the result be rounded up
-     * @param timeZone  an optional timezone that should be applied before returning the milliseconds since the epoch
+     * @param tz        an optional timezone that should be applied before returning the milliseconds since the epoch
      * @return          the parsed date in milliseconds since the epoch
      */
-    public long parse(String text, LongSupplier now, boolean roundUp, ZoneId timeZone) {
-        long time;
-        String mathString;
-        if (text.startsWith("now")) {
-            try {
-                time = now.getAsLong();
-            } catch (Exception e) {
-                throw new ElasticsearchParseException("could not read the current timestamp", e);
-            }
-            mathString = text.substring("now".length());
-        } else {
-            int index = text.indexOf("||");
-            if (index == -1) {
-                return parseDateTime(text, timeZone, roundUp);
-            }
-            time = parseDateTime(text.substring(0, index), timeZone, false);
-            mathString = text.substring(index + 2);
-        }
-
-        return parseMath(mathString, time, roundUp, timeZone);
-    }
-
-    private long parseMath(final String mathString, final long time, final boolean roundUp,
-                           ZoneId timeZone) throws ElasticsearchParseException {
-        if (timeZone == null) {
-            timeZone = ZoneOffset.UTC;
-        }
-        ZonedDateTime dateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(time), timeZone);
-        for (int i = 0; i < mathString.length(); ) {
-            char c = mathString.charAt(i++);
-            final boolean round;
-            final int sign;
-            if (c == '/') {
-                round = true;
-                sign = 1;
-            } else {
-                round = false;
-                if (c == '+') {
-                    sign = 1;
-                } else if (c == '-') {
-                    sign = -1;
-                } else {
-                    throw new ElasticsearchParseException("operator not supported for date math [{}]", mathString);
-                }
-            }
-
-            if (i >= mathString.length()) {
-                throw new ElasticsearchParseException("truncated date math [{}]", mathString);
-            }
-
-            final int num;
-            if (!Character.isDigit(mathString.charAt(i))) {
-                num = 1;
-            } else {
-                int numFrom = i;
-                while (i < mathString.length() && Character.isDigit(mathString.charAt(i))) {
-                    i++;
-                }
-                if (i >= mathString.length()) {
-                    throw new ElasticsearchParseException("truncated date math [{}]", mathString);
-                }
-                num = Integer.parseInt(mathString.substring(numFrom, i));
-            }
-            if (round) {
-                if (num != 1) {
-                    throw new ElasticsearchParseException("rounding `/` can only be used on single unit types [{}]", mathString);
-                }
-            }
-            char unit = mathString.charAt(i++);
-            switch (unit) {
-                case 'y':
-                    if (round) {
-                        dateTime = dateTime.withDayOfYear(1).with(LocalTime.MIN);
-                    } else {
-                        dateTime = dateTime.plusYears(sign * num);
-                    }
-                    if (roundUp) {
-                        dateTime = dateTime.plusYears(1);
-                    }
-                    break;
-                case 'M':
-                    if (round) {
-                        dateTime = dateTime.withDayOfMonth(1).with(LocalTime.MIN);
-                    } else {
-                        dateTime = dateTime.plusMonths(sign * num);
-                    }
-                    if (roundUp) {
-                        dateTime = dateTime.plusMonths(1);
-                    }
-                    break;
-                case 'w':
-                    if (round) {
-                        dateTime = dateTime.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).with(LocalTime.MIN);
-                    } else {
-                        dateTime = dateTime.plusWeeks(sign * num);
-                    }
-                    if (roundUp) {
-                        dateTime = dateTime.plusWeeks(1);
-                    }
-                    break;
-                case 'd':
-                    if (round) {
-                        dateTime = dateTime.with(LocalTime.MIN);
-                    } else {
-                        dateTime = dateTime.plusDays(sign * num);
-                    }
-                    if (roundUp) {
-                        dateTime = dateTime.plusDays(1);
-                    }
-                    break;
-                case 'h':
-                case 'H':
-                    if (round) {
-                        dateTime = dateTime.withMinute(0).withSecond(0).withNano(0);
-                    } else {
-                        dateTime = dateTime.plusHours(sign * num);
-                    }
-                    if (roundUp) {
-                        dateTime = dateTime.plusHours(1);
-                    }
-                    break;
-                case 'm':
-                    if (round) {
-                        dateTime = dateTime.withSecond(0).withNano(0);
-                    } else {
-                        dateTime = dateTime.plusMinutes(sign * num);
-                    }
-                    if (roundUp) {
-                        dateTime = dateTime.plusMinutes(1);
-                    }
-                    break;
-                case 's':
-                    if (round) {
-                        dateTime = dateTime.withNano(0);
-                    } else {
-                        dateTime = dateTime.plusSeconds(sign * num);
-                    }
-                    if (roundUp) {
-                        dateTime = dateTime.plusSeconds(1);
-                    }
-                    break;
-                default:
-                    throw new ElasticsearchParseException("unit [{}] not supported for date math [{}]", unit, mathString);
-            }
-            if (roundUp) {
-                dateTime = dateTime.minus(1, ChronoField.MILLI_OF_SECOND.getBaseUnit());
-            }
-        }
-        return dateTime.toInstant().toEpochMilli();
-    }
-
-    private long parseDateTime(String value, ZoneId timeZone, boolean roundUpIfNoTime) {
-        DateFormatter formatter = roundUpIfNoTime ? this.roundUpFormatter : this.formatter;
-        try {
-            if (timeZone == null) {
-                return DateFormatters.toZonedDateTime(formatter.parse(value)).toInstant().toEpochMilli();
-            } else {
-                TemporalAccessor accessor = formatter.parse(value);
-                ZoneId zoneId = TemporalQueries.zone().queryFrom(accessor);
-                if (zoneId != null) {
-                    timeZone = zoneId;
-                }
-
-                return DateFormatters.toZonedDateTime(accessor).withZoneSameLocal(timeZone).toInstant().toEpochMilli();
-            }
-        } catch (IllegalArgumentException | DateTimeException e) {
-            throw new ElasticsearchParseException("failed to parse date field [{}]: [{}]", e, value, e.getMessage());
-        }
-    }
+    long parse(String text, LongSupplier now, boolean roundUp, ZoneId tz);
 }

+ 70 - 0
server/src/main/java/org/elasticsearch/common/time/DateUtils.java

@@ -0,0 +1,70 @@
+/*
+ * 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.apache.logging.log4j.LogManager;
+import org.elasticsearch.common.logging.DeprecationLogger;
+import org.joda.time.DateTimeZone;
+
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class DateUtils {
+    public static DateTimeZone zoneIdToDateTimeZone(ZoneId zoneId) {
+        if (zoneId == null) {
+            return null;
+        }
+        if (zoneId instanceof ZoneOffset) {
+            // the id for zoneoffset is not ISO compatible, so cannot be read by ZoneId.of
+            return DateTimeZone.forOffsetMillis(((ZoneOffset)zoneId).getTotalSeconds() * 1000);
+        }
+        return DateTimeZone.forID(zoneId.getId());
+    }
+
+    private static final DeprecationLogger DEPRECATION_LOGGER = new DeprecationLogger(LogManager.getLogger(DateFormatters.class));
+    // pkg private for tests
+    static final Map<String, String> DEPRECATED_SHORT_TIMEZONES;
+    static {
+        Map<String, String> tzs = new HashMap<>();
+        tzs.put("EST", "-05:00"); // eastern time without daylight savings
+        tzs.put("HST", "-10:00");
+        tzs.put("MST", "-07:00");
+        tzs.put("ROC", "Asia/Taipei");
+        tzs.put("Eire", "Europe/London");
+        DEPRECATED_SHORT_TIMEZONES = Collections.unmodifiableMap(tzs);
+    }
+
+    public static ZoneId dateTimeZoneToZoneId(DateTimeZone timeZone) {
+        if (timeZone == null) {
+            return null;
+        }
+
+        String deprecatedId = DEPRECATED_SHORT_TIMEZONES.get(timeZone.getID());
+        if (deprecatedId != null) {
+            DEPRECATION_LOGGER.deprecatedAndMaybeLog("timezone",
+                "Use of short timezone id " + timeZone.getID() + " is deprecated. Use " + deprecatedId + " instead");
+            return ZoneId.of(deprecatedId);
+        }
+        return ZoneId.of(timeZone.getID());
+    }
+}

+ 240 - 0
server/src/main/java/org/elasticsearch/common/time/JavaDateMathParser.java

@@ -0,0 +1,240 @@
+/*
+ * 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.ElasticsearchParseException;
+
+import java.time.DateTimeException;
+import java.time.DayOfWeek;
+import java.time.Instant;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+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;
+
+/**
+ * A parser for date/time formatted text with optional date math.
+ *
+ * The format of the datetime is configurable, and unix timestamps can also be used. Datemath
+ * is appended to a datetime with the following syntax:
+ * <code>||[+-/](\d+)?[yMwdhHms]</code>.
+ */
+public class JavaDateMathParser implements DateMathParser {
+
+    // base fields which should be used for default parsing, when we round up
+    private static final Map<TemporalField, Long> 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) {
+        Objects.requireNonNull(formatter);
+        this.formatter = formatter;
+        this.roundUpFormatter = formatter.parseDefaulting(ROUND_UP_BASE_FIELDS);
+    }
+
+    @Override
+    public long parse(String text, LongSupplier now, boolean roundUp, ZoneId timeZone) {
+        long time;
+        String mathString;
+        if (text.startsWith("now")) {
+            try {
+                time = now.getAsLong();
+            } catch (Exception e) {
+                throw new ElasticsearchParseException("could not read the current timestamp", e);
+            }
+            mathString = text.substring("now".length());
+        } else {
+            int index = text.indexOf("||");
+            if (index == -1) {
+                return parseDateTime(text, timeZone, roundUp);
+            }
+            time = parseDateTime(text.substring(0, index), timeZone, false);
+            mathString = text.substring(index + 2);
+        }
+
+        return parseMath(mathString, time, roundUp, timeZone);
+    }
+
+    private long parseMath(final String mathString, final long time, final boolean roundUp,
+                           ZoneId timeZone) throws ElasticsearchParseException {
+        if (timeZone == null) {
+            timeZone = ZoneOffset.UTC;
+        }
+        ZonedDateTime dateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(time), timeZone);
+        for (int i = 0; i < mathString.length(); ) {
+            char c = mathString.charAt(i++);
+            final boolean round;
+            final int sign;
+            if (c == '/') {
+                round = true;
+                sign = 1;
+            } else {
+                round = false;
+                if (c == '+') {
+                    sign = 1;
+                } else if (c == '-') {
+                    sign = -1;
+                } else {
+                    throw new ElasticsearchParseException("operator not supported for date math [{}]", mathString);
+                }
+            }
+
+            if (i >= mathString.length()) {
+                throw new ElasticsearchParseException("truncated date math [{}]", mathString);
+            }
+
+            final int num;
+            if (!Character.isDigit(mathString.charAt(i))) {
+                num = 1;
+            } else {
+                int numFrom = i;
+                while (i < mathString.length() && Character.isDigit(mathString.charAt(i))) {
+                    i++;
+                }
+                if (i >= mathString.length()) {
+                    throw new ElasticsearchParseException("truncated date math [{}]", mathString);
+                }
+                num = Integer.parseInt(mathString.substring(numFrom, i));
+            }
+            if (round) {
+                if (num != 1) {
+                    throw new ElasticsearchParseException("rounding `/` can only be used on single unit types [{}]", mathString);
+                }
+            }
+            char unit = mathString.charAt(i++);
+            switch (unit) {
+                case 'y':
+                    if (round) {
+                        dateTime = dateTime.withDayOfYear(1).with(LocalTime.MIN);
+                    } else {
+                        dateTime = dateTime.plusYears(sign * num);
+                    }
+                    if (roundUp) {
+                        dateTime = dateTime.plusYears(1);
+                    }
+                    break;
+                case 'M':
+                    if (round) {
+                        dateTime = dateTime.withDayOfMonth(1).with(LocalTime.MIN);
+                    } else {
+                        dateTime = dateTime.plusMonths(sign * num);
+                    }
+                    if (roundUp) {
+                        dateTime = dateTime.plusMonths(1);
+                    }
+                    break;
+                case 'w':
+                    if (round) {
+                        dateTime = dateTime.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).with(LocalTime.MIN);
+                    } else {
+                        dateTime = dateTime.plusWeeks(sign * num);
+                    }
+                    if (roundUp) {
+                        dateTime = dateTime.plusWeeks(1);
+                    }
+                    break;
+                case 'd':
+                    if (round) {
+                        dateTime = dateTime.with(LocalTime.MIN);
+                    } else {
+                        dateTime = dateTime.plusDays(sign * num);
+                    }
+                    if (roundUp) {
+                        dateTime = dateTime.plusDays(1);
+                    }
+                    break;
+                case 'h':
+                case 'H':
+                    if (round) {
+                        dateTime = dateTime.withMinute(0).withSecond(0).withNano(0);
+                    } else {
+                        dateTime = dateTime.plusHours(sign * num);
+                    }
+                    if (roundUp) {
+                        dateTime = dateTime.plusHours(1);
+                    }
+                    break;
+                case 'm':
+                    if (round) {
+                        dateTime = dateTime.withSecond(0).withNano(0);
+                    } else {
+                        dateTime = dateTime.plusMinutes(sign * num);
+                    }
+                    if (roundUp) {
+                        dateTime = dateTime.plusMinutes(1);
+                    }
+                    break;
+                case 's':
+                    if (round) {
+                        dateTime = dateTime.withNano(0);
+                    } else {
+                        dateTime = dateTime.plusSeconds(sign * num);
+                    }
+                    if (roundUp) {
+                        dateTime = dateTime.plusSeconds(1);
+                    }
+                    break;
+                default:
+                    throw new ElasticsearchParseException("unit [{}] not supported for date math [{}]", unit, mathString);
+            }
+            if (roundUp) {
+                dateTime = dateTime.minus(1, ChronoField.MILLI_OF_SECOND.getBaseUnit());
+            }
+        }
+        return dateTime.toInstant().toEpochMilli();
+    }
+
+    private long parseDateTime(String value, ZoneId timeZone, boolean roundUpIfNoTime) {
+        DateFormatter formatter = roundUpIfNoTime ? this.roundUpFormatter : this.formatter;
+        try {
+            if (timeZone == null) {
+                return DateFormatters.toZonedDateTime(formatter.parse(value)).toInstant().toEpochMilli();
+            } else {
+                TemporalAccessor accessor = formatter.parse(value);
+                ZoneId zoneId = TemporalQueries.zone().queryFrom(accessor);
+                if (zoneId != null) {
+                    timeZone = zoneId;
+                }
+
+                return DateFormatters.toZonedDateTime(accessor).withZoneSameLocal(timeZone).toInstant().toEpochMilli();
+            }
+        } catch (IllegalArgumentException | DateTimeException e) {
+            throw new ElasticsearchParseException("failed to parse date field [{}]: [{}]", e, value, e.getMessage());
+        }
+    }
+}

+ 10 - 9
server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java

@@ -36,10 +36,11 @@ import org.apache.lucene.util.BytesRef;
 import org.elasticsearch.common.Explicit;
 import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.geo.ShapeRelation;
-import org.elasticsearch.common.joda.DateMathParser;
 import org.elasticsearch.common.joda.FormatDateTimeFormatter;
 import org.elasticsearch.common.joda.Joda;
 import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.time.DateMathParser;
+import org.elasticsearch.common.time.DateUtils;
 import org.elasticsearch.common.util.LocaleUtils;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.support.XContentMapValues;
@@ -231,7 +232,7 @@ public class DateFieldMapper extends FieldMapper {
         public void setDateTimeFormatter(FormatDateTimeFormatter dateTimeFormatter) {
             checkIfFrozen();
             this.dateTimeFormatter = dateTimeFormatter;
-            this.dateMathParser = new DateMathParser(dateTimeFormatter);
+            this.dateMathParser = dateTimeFormatter.toDateMathParser();
         }
 
         protected DateMathParser dateMathParser() {
@@ -262,7 +263,7 @@ public class DateFieldMapper extends FieldMapper {
 
         @Override
         public Query rangeQuery(Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper, ShapeRelation relation,
-                @Nullable DateTimeZone timeZone, @Nullable DateMathParser forcedDateParser, QueryShardContext context) {
+                                @Nullable DateTimeZone timeZone, @Nullable DateMathParser forcedDateParser, QueryShardContext context) {
             failIfNotIndexed();
             if (relation == ShapeRelation.DISJOINT) {
                 throw new IllegalArgumentException("Field [" + name() + "] of type [" + typeName() +
@@ -296,8 +297,8 @@ public class DateFieldMapper extends FieldMapper {
             return query;
         }
 
-        public long parseToMilliseconds(Object value, boolean roundUp,
-                @Nullable DateTimeZone zone, @Nullable DateMathParser forcedDateParser, QueryRewriteContext context) {
+        public long parseToMilliseconds(Object value, boolean roundUp, @Nullable DateTimeZone zone,
+                                        @Nullable DateMathParser forcedDateParser, QueryRewriteContext context) {
             DateMathParser dateParser = dateMathParser();
             if (forcedDateParser != null) {
                 dateParser = forcedDateParser;
@@ -309,13 +310,13 @@ public class DateFieldMapper extends FieldMapper {
             } else {
                 strValue = value.toString();
             }
-            return dateParser.parse(strValue, context::nowInMillis, roundUp, zone);
+            return dateParser.parse(strValue, context::nowInMillis, roundUp, DateUtils.dateTimeZoneToZoneId(zone));
         }
 
         @Override
-        public Relation isFieldWithinQuery(IndexReader reader,
-                Object from, Object to, boolean includeLower, boolean includeUpper,
-                DateTimeZone timeZone, DateMathParser dateParser, QueryRewriteContext context) throws IOException {
+        public Relation isFieldWithinQuery(IndexReader reader, Object from, Object to, boolean includeLower, boolean includeUpper,
+                                           DateTimeZone timeZone, DateMathParser dateParser,
+                                           QueryRewriteContext context) throws IOException {
             if (dateParser == null) {
                 dateParser = this.dateMathParser;
             }

+ 1 - 1
server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java

@@ -38,7 +38,7 @@ import org.apache.lucene.util.BytesRef;
 import org.elasticsearch.ElasticsearchParseException;
 import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.geo.ShapeRelation;
-import org.elasticsearch.common.joda.DateMathParser;
+import org.elasticsearch.common.time.DateMathParser;
 import org.elasticsearch.common.unit.Fuzziness;
 import org.elasticsearch.index.analysis.NamedAnalyzer;
 import org.elasticsearch.index.fielddata.IndexFieldData;

+ 9 - 6
server/src/main/java/org/elasticsearch/index/mapper/RangeFieldMapper.java

@@ -44,11 +44,12 @@ import org.elasticsearch.common.Explicit;
 import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.collect.Tuple;
 import org.elasticsearch.common.geo.ShapeRelation;
-import org.elasticsearch.common.joda.DateMathParser;
 import org.elasticsearch.common.joda.FormatDateTimeFormatter;
 import org.elasticsearch.common.network.InetAddresses;
 import org.elasticsearch.common.settings.Setting;
 import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.time.DateMathParser;
+import org.elasticsearch.common.time.DateUtils;
 import org.elasticsearch.common.util.LocaleUtils;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentParser;
@@ -60,6 +61,7 @@ import org.joda.time.DateTimeZone;
 import java.io.IOException;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
+import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -257,7 +259,7 @@ public class RangeFieldMapper extends FieldMapper {
         public void setDateTimeFormatter(FormatDateTimeFormatter dateTimeFormatter) {
             checkIfFrozen();
             this.dateTimeFormatter = dateTimeFormatter;
-            this.dateMathParser = new DateMathParser(dateTimeFormatter);
+            this.dateMathParser = dateTimeFormatter.toDateMathParser();
         }
 
         protected DateMathParser dateMathParser() {
@@ -587,15 +589,16 @@ public class RangeFieldMapper extends FieldMapper {
             public Query rangeQuery(String field, boolean hasDocValues, Object lowerTerm, Object upperTerm, boolean includeLower,
                                     boolean includeUpper, ShapeRelation relation, @Nullable DateTimeZone timeZone,
                                     @Nullable DateMathParser parser, QueryShardContext context) {
-                DateTimeZone zone = (timeZone == null) ? DateTimeZone.UTC : timeZone;
+                    DateTimeZone zone = (timeZone == null) ? DateTimeZone.UTC : timeZone;
+                    ZoneId zoneId = DateUtils.dateTimeZoneToZoneId(zone);
                 DateMathParser dateMathParser = (parser == null) ?
-                    new DateMathParser(DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER) : parser;
+                    DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.toDateMathParser() : parser;
                 Long low = lowerTerm == null ? Long.MIN_VALUE :
                     dateMathParser.parse(lowerTerm instanceof BytesRef ? ((BytesRef) lowerTerm).utf8ToString() : lowerTerm.toString(),
-                        context::nowInMillis, false, zone);
+                        context::nowInMillis, false, zoneId);
                 Long high = upperTerm == null ? Long.MAX_VALUE :
                     dateMathParser.parse(upperTerm instanceof BytesRef ? ((BytesRef) upperTerm).utf8ToString() : upperTerm.toString(),
-                        context::nowInMillis, false, zone);
+                        context::nowInMillis, false, zoneId);
 
                 return super.rangeQuery(field, hasDocValues, low, high, includeLower, includeUpper, relation, zone,
                     dateMathParser, context);

+ 2 - 2
server/src/main/java/org/elasticsearch/index/mapper/SimpleMappedFieldType.java

@@ -21,7 +21,7 @@ package org.elasticsearch.index.mapper;
 
 import org.apache.lucene.search.Query;
 import org.elasticsearch.common.geo.ShapeRelation;
-import org.elasticsearch.common.joda.DateMathParser;
+import org.elasticsearch.common.time.DateMathParser;
 import org.elasticsearch.index.query.QueryShardContext;
 import org.joda.time.DateTimeZone;
 
@@ -40,7 +40,7 @@ public abstract class SimpleMappedFieldType extends MappedFieldType {
 
     @Override
     public final Query rangeQuery(Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper,
-            ShapeRelation relation, DateTimeZone timeZone, DateMathParser parser, QueryShardContext context) {
+                                  ShapeRelation relation, DateTimeZone timeZone, DateMathParser parser, QueryShardContext context) {
         if (relation == ShapeRelation.DISJOINT) {
             throw new IllegalArgumentException("Field [" + name() + "] of type [" + typeName() +
                     "] does not support DISJOINT ranges");

+ 2 - 2
server/src/main/java/org/elasticsearch/index/query/RangeQueryBuilder.java

@@ -29,10 +29,10 @@ import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.geo.ShapeRelation;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
-import org.elasticsearch.common.joda.DateMathParser;
 import org.elasticsearch.common.joda.FormatDateTimeFormatter;
 import org.elasticsearch.common.joda.Joda;
 import org.elasticsearch.common.lucene.BytesRefs;
+import org.elasticsearch.common.time.DateMathParser;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.index.mapper.FieldNamesFieldMapper;
@@ -302,7 +302,7 @@ public class RangeQueryBuilder extends AbstractQueryBuilder<RangeQueryBuilder> i
 
     DateMathParser getForceDateParser() { // pkg private for testing
         if (this.format != null) {
-            return new DateMathParser(this.format);
+            return this.format.toDateMathParser();
         }
         return null;
     }

+ 5 - 3
server/src/main/java/org/elasticsearch/search/DocValueFormat.java

@@ -25,11 +25,12 @@ import org.elasticsearch.common.geo.GeoHashUtils;
 import org.elasticsearch.common.io.stream.NamedWriteable;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
-import org.elasticsearch.common.joda.DateMathParser;
 import org.elasticsearch.common.joda.FormatDateTimeFormatter;
 import org.elasticsearch.common.joda.Joda;
 import org.elasticsearch.common.network.InetAddresses;
 import org.elasticsearch.common.network.NetworkAddress;
+import org.elasticsearch.common.time.DateMathParser;
+import org.elasticsearch.common.time.DateUtils;
 import org.joda.time.DateTimeZone;
 
 import java.io.IOException;
@@ -171,13 +172,14 @@ public interface DocValueFormat extends NamedWriteable {
         public static final String NAME = "date_time";
 
         final FormatDateTimeFormatter formatter;
+        // TODO: change this to ZoneId, but will require careful change to serialization
         final DateTimeZone timeZone;
         private final DateMathParser parser;
 
         public DateTime(FormatDateTimeFormatter formatter, DateTimeZone timeZone) {
             this.formatter = Objects.requireNonNull(formatter);
             this.timeZone = Objects.requireNonNull(timeZone);
-            this.parser = new DateMathParser(formatter);
+            this.parser = formatter.toDateMathParser();
         }
 
         public DateTime(StreamInput in) throws IOException {
@@ -212,7 +214,7 @@ public interface DocValueFormat extends NamedWriteable {
 
         @Override
         public long parseLong(String value, boolean roundUp, LongSupplier now) {
-            return parser.parse(value, now, roundUp, timeZone);
+            return parser.parse(value, now, roundUp, DateUtils.dateTimeZoneToZoneId(timeZone));
         }
 
         @Override

+ 2 - 2
server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramAggregationBuilder.java

@@ -25,10 +25,10 @@ import org.apache.lucene.index.SortedNumericDocValues;
 import org.apache.lucene.search.DocIdSetIterator;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
-import org.elasticsearch.common.joda.DateMathParser;
 import org.elasticsearch.common.joda.Joda;
 import org.elasticsearch.common.rounding.DateTimeUnit;
 import org.elasticsearch.common.rounding.Rounding;
+import org.elasticsearch.common.time.DateMathParser;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.common.xcontent.ObjectParser;
 import org.elasticsearch.common.xcontent.XContentBuilder;
@@ -72,7 +72,7 @@ import static java.util.Collections.unmodifiableMap;
 public class DateHistogramAggregationBuilder extends ValuesSourceAggregationBuilder<ValuesSource.Numeric, DateHistogramAggregationBuilder>
         implements MultiBucketAggregationBuilder {
     public static final String NAME = "date_histogram";
-    private static DateMathParser EPOCH_MILLIS_PARSER = new DateMathParser(Joda.forPattern("epoch_millis", Locale.ROOT));
+    private static DateMathParser EPOCH_MILLIS_PARSER = Joda.forPattern("epoch_millis", Locale.ROOT).toDateMathParser();
 
     public static final Map<String, DateTimeUnit> DATE_FIELD_UNITS;
 

+ 15 - 15
server/src/test/java/org/elasticsearch/common/joda/DateMathParserTests.java → server/src/test/java/org/elasticsearch/common/joda/JodaDateMathParserTests.java

@@ -24,17 +24,17 @@ import org.elasticsearch.ExceptionsHelper;
 import org.elasticsearch.test.ESTestCase;
 import org.joda.time.DateTimeZone;
 
-import java.util.TimeZone;
+import java.time.ZoneId;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.LongSupplier;
 
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
 
-public class DateMathParserTests extends ESTestCase {
+public class JodaDateMathParserTests extends ESTestCase {
 
     FormatDateTimeFormatter formatter = Joda.forPattern("dateOptionalTime||epoch_millis");
-    DateMathParser parser = new DateMathParser(formatter);
+    JodaDateMathParser parser = new JodaDateMathParser(formatter);
 
     void assertDateMathEquals(String toTest, String expected) {
         assertDateMathEquals(toTest, expected, 0, false, null);
@@ -145,7 +145,7 @@ public class DateMathParserTests extends ESTestCase {
 
 
     public void testNow() {
-        final long now = parser.parse("2014-11-18T14:27:32", () -> 0, false, null);
+        final long now = parser.parse("2014-11-18T14:27:32", () -> 0, false, (ZoneId) null);
 
         assertDateMathEquals("now", "2014-11-18T14:27:32", now, false, null);
         assertDateMathEquals("now+M", "2014-12-18T14:27:32", now, false, null);
@@ -159,13 +159,13 @@ public class DateMathParserTests extends ESTestCase {
     public void testRoundingPreservesEpochAsBaseDate() {
         // If a user only specifies times, then the date needs to always be 1970-01-01 regardless of rounding
         FormatDateTimeFormatter formatter = Joda.forPattern("HH:mm:ss");
-        DateMathParser parser = new DateMathParser(formatter);
+        JodaDateMathParser parser = new JodaDateMathParser(formatter);
         assertEquals(
                 this.formatter.parser().parseMillis("1970-01-01T04:52:20.000Z"),
-                parser.parse("04:52:20", () -> 0, false, null));
+                parser.parse("04:52:20", () -> 0, false, (ZoneId) null));
         assertEquals(
                 this.formatter.parser().parseMillis("1970-01-01T04:52:20.999Z"),
-                parser.parse("04:52:20", () -> 0, true, null));
+                parser.parse("04:52:20", () -> 0, true, (ZoneId) null));
     }
 
     // Implicit rounding happening when parts of the date are not specified
@@ -184,10 +184,10 @@ public class DateMathParserTests extends ESTestCase {
 
         // implicit rounding with explicit timezone in the date format
         FormatDateTimeFormatter formatter = Joda.forPattern("YYYY-MM-ddZ");
-        DateMathParser parser = new DateMathParser(formatter);
-        long time = parser.parse("2011-10-09+01:00", () -> 0, false, null);
+        JodaDateMathParser parser = new JodaDateMathParser(formatter);
+        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, null);
+        time = parser.parse("2011-10-09+01:00", () -> 0, true, (ZoneId) null);
         assertEquals(this.parser.parse("2011-10-09T23:59:59.999+01:00", () -> 0), time);
     }
 
@@ -258,7 +258,7 @@ public class DateMathParserTests extends ESTestCase {
         assertDateMathEquals("1418248078000||/m", "2014-12-10T21:47:00.000");
 
         // also check other time units
-        DateMathParser parser = new DateMathParser(Joda.forPattern("epoch_second||dateOptionalTime"));
+        JodaDateMathParser parser = new JodaDateMathParser(Joda.forPattern("epoch_second||dateOptionalTime"));
         long datetime = parser.parse("1418248078", () -> 0);
         assertDateEquals(datetime, "1418248078", "2014-12-10T21:47:58.000");
 
@@ -298,16 +298,16 @@ public class DateMathParserTests extends ESTestCase {
             called.set(true);
             return 42L;
         };
-        parser.parse("2014-11-18T14:27:32", now, false, null);
+        parser.parse("2014-11-18T14:27:32", now, false, (ZoneId) null);
         assertFalse(called.get());
-        parser.parse("now/d", now, false, null);
+        parser.parse("now/d", now, false, (ZoneId) null);
         assertTrue(called.get());
     }
 
     public void testThatUnixTimestampMayNotHaveTimeZone() {
-        DateMathParser parser = new DateMathParser(Joda.forPattern("epoch_millis"));
+        JodaDateMathParser parser = new JodaDateMathParser(Joda.forPattern("epoch_millis"));
         try {
-            parser.parse("1234567890123", () -> 42, false, DateTimeZone.forTimeZone(TimeZone.getTimeZone("CET")));
+            parser.parse("1234567890123", () -> 42, false, ZoneId.of("CET"));
             fail("Expected ElasticsearchParseException");
         } catch(ElasticsearchParseException e) {
             assertThat(e.getMessage(), containsString("failed to parse date field"));

+ 54 - 0
server/src/test/java/org/elasticsearch/common/time/DateUtilsTests.java

@@ -0,0 +1,54 @@
+/*
+ * 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 org.joda.time.DateTimeZone;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class DateUtilsTests extends ESTestCase {
+    private static final Set<String> IGNORE = new HashSet<>(Arrays.asList(
+        "Eire", "Europe/Dublin" // dublin timezone in joda does not account for DST
+    ));
+    public void testTimezoneIds() {
+        assertNull(DateUtils.dateTimeZoneToZoneId(null));
+        assertNull(DateUtils.zoneIdToDateTimeZone(null));
+        for (String jodaId : DateTimeZone.getAvailableIDs()) {
+            if (IGNORE.contains(jodaId)) continue;
+            DateTimeZone jodaTz = DateTimeZone.forID(jodaId);
+            ZoneId zoneId = DateUtils.dateTimeZoneToZoneId(jodaTz); // does not throw
+            long now = 0;
+            assertThat(jodaId, zoneId.getRules().getOffset(Instant.ofEpochMilli(now)).getTotalSeconds() * 1000,
+                equalTo(jodaTz.getOffset(now)));
+            if (DateUtils.DEPRECATED_SHORT_TIMEZONES.containsKey(jodaTz.getID())) {
+                assertWarnings("Use of short timezone id " + jodaId + " is deprecated. Use " + zoneId.getId() + " instead");
+            }
+            // roundtrip does not throw either
+            assertNotNull(DateUtils.zoneIdToDateTimeZone(zoneId));
+        }
+    }
+}

+ 12 - 12
server/src/test/java/org/elasticsearch/common/time/DateMathParserTests.java → server/src/test/java/org/elasticsearch/common/time/JavaDateMathParserTests.java

@@ -33,10 +33,10 @@ import java.util.function.LongSupplier;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.is;
 
-public class DateMathParserTests extends ESTestCase {
+public class JavaDateMathParserTests extends ESTestCase {
 
     private final DateFormatter formatter = DateFormatters.forPattern("dateOptionalTime||epoch_millis");
-    private final DateMathParser parser = new DateMathParser(formatter);
+    private final JavaDateMathParser parser = new JavaDateMathParser(formatter);
 
     public void testBasicDates() {
         assertDateMathEquals("2014", "2014-01-01T00:00:00.000");
@@ -125,7 +125,7 @@ public class DateMathParserTests extends ESTestCase {
     }
 
     public void testNow() {
-        final long now = parser.parse("2014-11-18T14:27:32", () -> 0, false, null);
+        final long now = parser.parse("2014-11-18T14:27:32", () -> 0, false, (ZoneId) null);
 
         assertDateMathEquals("now", "2014-11-18T14:27:32", now, false, null);
         assertDateMathEquals("now+M", "2014-12-18T14:27:32", now, false, null);
@@ -139,14 +139,14 @@ public class DateMathParserTests extends ESTestCase {
     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");
-        DateMathParser parser = new DateMathParser(formatter);
+        JavaDateMathParser parser = new JavaDateMathParser(formatter);
         ZonedDateTime zonedDateTime = DateFormatters.toZonedDateTime(formatter.parse("04:52:20"));
         assertThat(zonedDateTime.getYear(), is(1970));
         long millisStart = zonedDateTime.toInstant().toEpochMilli();
-        assertEquals(millisStart, parser.parse("04:52:20", () -> 0, false, null));
+        assertEquals(millisStart, parser.parse("04:52:20", () -> 0, false, (ZoneId) null));
         // due to rounding up, we have to add the number of milliseconds here manually
         long millisEnd = DateFormatters.toZonedDateTime(formatter.parse("04:52:20")).toInstant().toEpochMilli() + 999;
-        assertEquals(millisEnd, parser.parse("04:52:20", () -> 0, true, null));
+        assertEquals(millisEnd, parser.parse("04:52:20", () -> 0, true, (ZoneId) null));
     }
 
     // Implicit rounding happening when parts of the date are not specified
@@ -165,10 +165,10 @@ public class DateMathParserTests extends ESTestCase {
 
         // implicit rounding with explicit timezone in the date format
         DateFormatter formatter = DateFormatters.forPattern("yyyy-MM-ddXXX");
-        DateMathParser parser = new DateMathParser(formatter);
-        long time = parser.parse("2011-10-09+01:00", () -> 0, false, null);
+        JavaDateMathParser parser = new JavaDateMathParser(formatter);
+        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, null);
+        time = parser.parse("2011-10-09+01:00", () -> 0, true, (ZoneId) null);
         assertEquals(this.parser.parse("2011-10-09T23:59:59.999+01:00", () -> 0), time);
     }
 
@@ -239,7 +239,7 @@ public class DateMathParserTests extends ESTestCase {
         assertDateMathEquals("1418248078000||/m", "2014-12-10T21:47:00.000");
 
         // also check other time units
-        DateMathParser parser = new DateMathParser(DateFormatters.forPattern("epoch_second||dateOptionalTime"));
+        JavaDateMathParser parser = new JavaDateMathParser(DateFormatters.forPattern("epoch_second||dateOptionalTime"));
         long datetime = parser.parse("1418248078", () -> 0);
         assertDateEquals(datetime, "1418248078", "2014-12-10T21:47:58.000");
 
@@ -279,9 +279,9 @@ public class DateMathParserTests extends ESTestCase {
             called.set(true);
             return 42L;
         };
-        parser.parse("2014-11-18T14:27:32", now, false, null);
+        parser.parse("2014-11-18T14:27:32", now, false, (ZoneId) null);
         assertFalse(called.get());
-        parser.parse("now/d", now, false, null);
+        parser.parse("now/d", now, false, (ZoneId) null);
         assertTrue(called.get());
     }
 

+ 3 - 3
server/src/test/java/org/elasticsearch/index/mapper/DateFieldTypeTests.java

@@ -29,12 +29,12 @@ import org.apache.lucene.index.MultiReader;
 import org.apache.lucene.search.IndexOrDocValuesQuery;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.store.Directory;
-import org.elasticsearch.core.internal.io.IOUtils;
 import org.elasticsearch.Version;
 import org.elasticsearch.cluster.metadata.IndexMetaData;
-import org.elasticsearch.common.joda.DateMathParser;
 import org.elasticsearch.common.joda.Joda;
 import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.time.DateMathParser;
+import org.elasticsearch.core.internal.io.IOUtils;
 import org.elasticsearch.index.IndexSettings;
 import org.elasticsearch.index.mapper.DateFieldMapper.DateFieldType;
 import org.elasticsearch.index.mapper.MappedFieldType.Relation;
@@ -121,7 +121,7 @@ public class DateFieldTypeTests extends FieldTypeTestCase {
         DirectoryReader reader = DirectoryReader.open(w);
         DateFieldType ft = new DateFieldType();
         ft.setName("my_date");
-        DateMathParser alternateFormat = new DateMathParser(DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER);
+        DateMathParser alternateFormat = DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.toDateMathParser();
         doTestIsFieldWithinQuery(ft, reader, null, null);
         doTestIsFieldWithinQuery(ft, reader, null, alternateFormat);
         doTestIsFieldWithinQuery(ft, reader, DateTimeZone.UTC, null);

+ 1 - 1
server/src/test/java/org/elasticsearch/index/mapper/RangeFieldQueryStringQueryBuilderTests.java

@@ -31,8 +31,8 @@ import org.apache.lucene.search.Query;
 import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.compress.CompressedXContent;
-import org.elasticsearch.common.joda.DateMathParser;
 import org.elasticsearch.common.network.InetAddresses;
+import org.elasticsearch.common.time.DateMathParser;
 import org.elasticsearch.index.query.QueryShardContext;
 import org.elasticsearch.index.query.QueryStringQueryBuilder;
 import org.elasticsearch.search.internal.SearchContext;

+ 2 - 2
server/src/test/java/org/elasticsearch/search/aggregations/bucket/DateHistogramIT.java

@@ -23,9 +23,9 @@ import org.elasticsearch.action.index.IndexRequestBuilder;
 import org.elasticsearch.action.search.SearchPhaseExecutionException;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.common.Strings;
-import org.elasticsearch.common.joda.DateMathParser;
 import org.elasticsearch.common.joda.Joda;
 import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.time.DateMathParser;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.index.mapper.DateFieldMapper;
 import org.elasticsearch.index.query.MatchNoneQueryBuilder;
@@ -1120,7 +1120,7 @@ public class DateHistogramIT extends ESIntegTestCase {
                 .setSettings(Settings.builder().put(indexSettings()).put("index.number_of_shards", 1).put("index.number_of_replicas", 0))
                 .execute().actionGet();
 
-        DateMathParser parser = new DateMathParser(Joda.getStrictStandardDateFormatter());
+        DateMathParser parser = Joda.getStrictStandardDateFormatter().toDateMathParser();
 
         // we pick a random timezone offset of +12/-12 hours and insert two documents
         // one at 00:00 in that time zone and one at 12:00

+ 1 - 1
test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java

@@ -408,7 +408,7 @@ public abstract class ESTestCase extends LuceneTestCase {
         }
         try {
             final List<String> actualWarnings = threadContext.getResponseHeaders().get("Warning");
-            assertNotNull(actualWarnings);
+            assertNotNull("no warnings, expected: " + Arrays.asList(expectedWarnings), actualWarnings);
             final Set<String> actualWarningValues =
                     actualWarnings.stream().map(DeprecationLogger::extractWarningValueFromWarningHeader).collect(Collectors.toSet());
             for (String msg : expectedWarnings) {

+ 2 - 3
x-pack/license-tools/src/test/java/org/elasticsearch/license/licensor/TestUtils.java

@@ -6,9 +6,9 @@
 package org.elasticsearch.license.licensor;
 
 import org.elasticsearch.common.Strings;
-import org.elasticsearch.common.joda.DateMathParser;
 import org.elasticsearch.common.joda.FormatDateTimeFormatter;
 import org.elasticsearch.common.joda.Joda;
+import org.elasticsearch.common.time.DateMathParser;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.common.xcontent.ToXContent;
 import org.elasticsearch.common.xcontent.XContentBuilder;
@@ -38,8 +38,7 @@ public class TestUtils {
 
     private static final FormatDateTimeFormatter formatDateTimeFormatter =
             Joda.forPattern("yyyy-MM-dd");
-    private static final DateMathParser dateMathParser =
-            new DateMathParser(formatDateTimeFormatter);
+    private static final DateMathParser dateMathParser = formatDateTimeFormatter.toDateMathParser();
     private static final DateTimeFormatter dateTimeFormatter = formatDateTimeFormatter.printer();
 
     public static String dumpLicense(License license) throws Exception {

+ 2 - 2
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetOverallBucketsAction.java

@@ -16,7 +16,7 @@ import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
-import org.elasticsearch.common.joda.DateMathParser;
+import org.elasticsearch.common.time.DateMathParser;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.common.xcontent.ObjectParser;
 import org.elasticsearch.common.xcontent.ToXContentObject;
@@ -88,7 +88,7 @@ public class GetOverallBucketsAction extends Action<GetOverallBucketsAction.Resp
         }
 
         static long parseDateOrThrow(String date, ParseField paramName, LongSupplier now) {
-            DateMathParser dateMathParser = new DateMathParser(DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER);
+            DateMathParser dateMathParser = DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.toDateMathParser();
 
             try {
                 return dateMathParser.parse(date, now);

+ 2 - 2
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StartDatafeedAction.java

@@ -17,7 +17,7 @@ import org.elasticsearch.client.ElasticsearchClient;
 import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
-import org.elasticsearch.common.joda.DateMathParser;
+import org.elasticsearch.common.time.DateMathParser;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.common.xcontent.ObjectParser;
 import org.elasticsearch.common.xcontent.ToXContent;
@@ -153,7 +153,7 @@ public class StartDatafeedAction extends Action<AcknowledgedResponse> {
         }
 
         static long parseDateOrThrow(String date, ParseField paramName, LongSupplier now) {
-            DateMathParser dateMathParser = new DateMathParser(DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER);
+            DateMathParser dateMathParser = DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.toDateMathParser();
 
             try {
                 return dateMathParser.parse(date, now);

+ 2 - 2
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/WatcherDateTimeUtils.java

@@ -9,8 +9,8 @@ import org.elasticsearch.ElasticsearchParseException;
 import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
-import org.elasticsearch.common.joda.DateMathParser;
 import org.elasticsearch.common.joda.FormatDateTimeFormatter;
+import org.elasticsearch.common.time.DateMathParser;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentParser;
@@ -26,7 +26,7 @@ import java.util.concurrent.TimeUnit;
 public class WatcherDateTimeUtils {
 
     public static final FormatDateTimeFormatter dateTimeFormatter = DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER;
-    public static final DateMathParser dateMathParser = new DateMathParser(dateTimeFormatter);
+    public static final DateMathParser dateMathParser = dateTimeFormatter.toDateMathParser();
 
     private WatcherDateTimeUtils() {
     }

+ 2 - 2
x-pack/plugin/core/src/test/java/org/elasticsearch/license/TestUtils.java

@@ -10,10 +10,10 @@ import org.elasticsearch.Version;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.cluster.metadata.MetaData;
 import org.elasticsearch.common.Strings;
-import org.elasticsearch.common.joda.DateMathParser;
 import org.elasticsearch.common.joda.FormatDateTimeFormatter;
 import org.elasticsearch.common.joda.Joda;
 import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.time.DateMathParser;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.common.xcontent.ToXContent;
 import org.elasticsearch.common.xcontent.XContentBuilder;
@@ -50,7 +50,7 @@ import static org.junit.Assert.assertThat;
 public class TestUtils {
 
     private static final FormatDateTimeFormatter formatDateTimeFormatter = Joda.forPattern("yyyy-MM-dd");
-    private static final DateMathParser dateMathParser = new DateMathParser(formatDateTimeFormatter);
+    private static final DateMathParser dateMathParser = formatDateTimeFormatter.toDateMathParser();
     private static final DateTimeFormatter dateTimeFormatter = formatDateTimeFormatter.printer();
 
     public static String dateMathString(String time, final long now) {

+ 2 - 3
x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/job/RollupIndexerIndexingTests.java

@@ -29,7 +29,6 @@ import org.elasticsearch.action.search.SearchRequest;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.action.search.SearchResponseSections;
 import org.elasticsearch.action.search.ShardSearchFailure;
-import org.elasticsearch.common.joda.DateMathParser;
 import org.elasticsearch.common.joda.Joda;
 import org.elasticsearch.common.rounding.Rounding;
 import org.elasticsearch.common.unit.TimeValue;
@@ -47,13 +46,13 @@ import org.elasticsearch.search.aggregations.AggregatorTestCase;
 import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregation;
 import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregationBuilder;
 import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval;
+import org.elasticsearch.xpack.core.indexing.IndexerState;
 import org.elasticsearch.xpack.core.rollup.ConfigTestHelpers;
 import org.elasticsearch.xpack.core.rollup.job.DateHistogramGroupConfig;
 import org.elasticsearch.xpack.core.rollup.job.GroupConfig;
 import org.elasticsearch.xpack.core.rollup.job.MetricConfig;
 import org.elasticsearch.xpack.core.rollup.job.RollupJob;
 import org.elasticsearch.xpack.core.rollup.job.RollupJobConfig;
-import org.elasticsearch.xpack.core.indexing.IndexerState;
 import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
 import org.junit.Before;
@@ -601,7 +600,7 @@ public class RollupIndexerIndexingTests extends AggregatorTestCase {
             RangeQueryBuilder range = (RangeQueryBuilder) request.source().query();
             final DateTimeZone timeZone = range.timeZone() != null ? DateTimeZone.forID(range.timeZone()) : null;
             Query query = timestampField.rangeQuery(range.from(), range.to(), range.includeLower(), range.includeUpper(),
-                    null, timeZone, new DateMathParser(Joda.forPattern(range.format())), queryShardContext);
+                    null, timeZone, Joda.forPattern(range.format()).toDateMathParser(), queryShardContext);
 
             // extract composite agg
             assertThat(request.source().aggregations().getAggregatorFactories().size(), equalTo(1));