Core: Add backcompat for joda time formats (#36531)

This commit adds deprecation warnings when using format specifiers with
joda data formats that will change with java time. It also adds the "8"
prefix which may be used to force the new java time format parsing.
This commit is contained in:
Ryan Ernst 2018-12-13 12:26:51 -08:00 committed by GitHub
parent b33ff16d62
commit 254d1e8f22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 90 additions and 57 deletions

View File

@ -33,7 +33,7 @@ PUT _template/template_1
},
"created_at": {
"type": "date",
"format": "EEE MMM dd HH:mm:ss Z YYYY"
"format": "EEE MMM dd HH:mm:ss Z yyyy"
}
}
}

View File

@ -19,7 +19,9 @@
package org.elasticsearch.common.joda;
import org.apache.logging.log4j.LogManager;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.time.DateFormatter;
import org.joda.time.Chronology;
import org.joda.time.DateTime;
@ -48,10 +50,12 @@ import java.util.Locale;
public class Joda {
private static DeprecationLogger deprecationLogger = new DeprecationLogger(LogManager.getLogger(Joda.class));
/**
* Parses a joda based pattern, including some named ones (similar to the built in Joda ISO ones).
*/
public static JodaDateFormatter forPattern(String input, Locale locale) {
public static JodaDateFormatter forPattern(String input) {
if (Strings.hasLength(input)) {
input = input.trim();
}
@ -102,8 +106,8 @@ public class Joda {
// in this case, we have a separate parser and printer since the dataOptionalTimeParser can't print
// this sucks we should use the root local by default and not be dependent on the node
return new JodaDateFormatter(input,
ISODateTimeFormat.dateOptionalTimeParser().withLocale(locale).withZone(DateTimeZone.UTC),
ISODateTimeFormat.dateTime().withLocale(locale).withZone(DateTimeZone.UTC));
ISODateTimeFormat.dateOptionalTimeParser().withLocale(Locale.ROOT).withZone(DateTimeZone.UTC),
ISODateTimeFormat.dateTime().withLocale(Locale.ROOT).withZone(DateTimeZone.UTC));
} else if ("dateTime".equals(input) || "date_time".equals(input)) {
formatter = ISODateTimeFormat.dateTime();
} else if ("dateTimeNoMillis".equals(input) || "date_time_no_millis".equals(input)) {
@ -179,8 +183,8 @@ public class Joda {
// in this case, we have a separate parser and printer since the dataOptionalTimeParser can't print
// this sucks we should use the root local by default and not be dependent on the node
return new JodaDateFormatter(input,
StrictISODateTimeFormat.dateOptionalTimeParser().withLocale(locale).withZone(DateTimeZone.UTC),
StrictISODateTimeFormat.dateTime().withLocale(locale).withZone(DateTimeZone.UTC));
StrictISODateTimeFormat.dateOptionalTimeParser().withLocale(Locale.ROOT).withZone(DateTimeZone.UTC),
StrictISODateTimeFormat.dateTime().withLocale(Locale.ROOT).withZone(DateTimeZone.UTC));
} else if ("strictDateTime".equals(input) || "strict_date_time".equals(input)) {
formatter = StrictISODateTimeFormat.dateTime();
} else if ("strictDateTimeNoMillis".equals(input) || "strict_date_time_no_millis".equals(input)) {
@ -227,19 +231,34 @@ public class Joda {
formatter = StrictISODateTimeFormat.yearMonth();
} else if ("strictYearMonthDay".equals(input) || "strict_year_month_day".equals(input)) {
formatter = StrictISODateTimeFormat.yearMonthDay();
} else {
try {
maybeLogJodaDeprecation(input);
formatter = DateTimeFormat.forPattern(input);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid format: [" + input + "]: " + e.getMessage(), e);
}
}
formatter = formatter.withLocale(locale).withZone(DateTimeZone.UTC);
formatter = formatter.withLocale(Locale.ROOT).withZone(DateTimeZone.UTC);
return new JodaDateFormatter(input, formatter, formatter);
}
private static void maybeLogJodaDeprecation(String input) {
if (input.contains("CC")) {
deprecationLogger.deprecatedAndMaybeLog("joda-century-of-era-format",
"Use of 'C' (century-of-era) is deprecated and will not be supported in the next major version of Elasticsearch.");
}
if (input.contains("YY")) {
deprecationLogger.deprecatedAndMaybeLog("joda-year-of-era-format", "Use of 'Y' (year-of-era) will change to 'y' in the" +
" next major version of Elasticsearch. Prefix your date format with '8' to use the new specifier.");
}
if (input.contains("xx")) {
deprecationLogger.deprecatedAndMaybeLog("joda-week-based-year-format","Use of 'x' (week-based-year) will change" +
" to 'Y' in the next major version of Elasticsearch. Prefix your date format with '8' to use the new specifier.");
}
}
public static DateFormatter getStrictStandardDateFormatter() {
// 2014/10/10
DateTimeFormatter shortFormatter = new DateTimeFormatterBuilder()

View File

@ -126,16 +126,22 @@ public interface DateFormatter {
DateMathParser toDateMathParser();
static DateFormatter forPattern(String input) {
return forPattern(input, Locale.ROOT);
}
static DateFormatter forPattern(String input, Locale locale) {
if (Strings.hasLength(input) == false) {
throw new IllegalArgumentException("No date pattern provided");
}
List<DateFormatter> formatters = new ArrayList<>();
for (String pattern : Strings.delimitedListToStringArray(input, "||")) {
formatters.add(Joda.forPattern(pattern, locale));
if (Strings.hasLength(input) == false) {
throw new IllegalArgumentException("Cannot have empty element in multi date format pattern: " + input);
}
final DateFormatter formatter;
if (pattern.startsWith("8")) {
// force java 8 date format
formatter = DateFormatters.forPattern(pattern.substring(1));
} else {
formatter = Joda.forPattern(pattern);
}
formatters.add(formatter);
}
if (formatters.size() == 1) {

View File

@ -39,7 +39,6 @@ import java.time.temporal.IsoFields;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalAdjusters;
import java.time.temporal.WeekFields;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@ -1273,10 +1272,6 @@ public class DateFormatters {
/////////////////////////////////////////
public static DateFormatter forPattern(String input) {
return forPattern(input, Locale.ROOT);
}
private static DateFormatter forPattern(String input, Locale locale) {
if (Strings.hasLength(input)) {
input = input.trim();
}
@ -1443,25 +1438,9 @@ public class DateFormatters {
return STRICT_YEAR_MONTH;
} else if ("strictYearMonthDay".equals(input) || "strict_year_month_day".equals(input)) {
return STRICT_YEAR_MONTH_DAY;
} else if (Strings.hasLength(input) && input.contains("||")) {
String[] formats = Strings.delimitedListToStringArray(input, "||");
if (formats.length == 1) {
return forPattern(formats[0], locale);
} else {
try {
List<DateFormatter> formatters = new ArrayList<>(formats.length);
for (int i = 0; i < formats.length; i++) {
formatters.add(forPattern(formats[i], locale));
}
return new MergedDateFormatter(input, formatters);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid format: [" + input + "]: " + e.getMessage(), e);
}
}
} else {
try {
return new JavaDateFormatter(input, new DateTimeFormatterBuilder().appendPattern(input).toFormatter(locale));
return new JavaDateFormatter(input, new DateTimeFormatterBuilder().appendPattern(input).toFormatter(Locale.ROOT));
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid format: [" + input + "]: " + e.getMessage(), e);
}
@ -1471,7 +1450,8 @@ public class DateFormatters {
static class MergedDateFormatter implements DateFormatter {
private final String pattern;
private final List<DateFormatter> formatters;
// package private for tests
final List<DateFormatter> formatters;
private final List<DateMathParser> dateMathParsers;
MergedDateFormatter(String pattern, List<DateFormatter> formatters) {

View File

@ -64,8 +64,7 @@ import static org.elasticsearch.index.mapper.TypeParsers.parseDateTimeFormatter;
public class DateFieldMapper extends FieldMapper {
public static final String CONTENT_TYPE = "date";
public static final DateFormatter DEFAULT_DATE_TIME_FORMATTER = DateFormatter.forPattern(
"strict_date_optional_time||epoch_millis", Locale.ROOT);
public static final DateFormatter DEFAULT_DATE_TIME_FORMATTER = DateFormatter.forPattern("strict_date_optional_time||epoch_millis");
public static class Defaults {
public static final Explicit<Boolean> IGNORE_MALFORMED = new Explicit<>(false, false);

View File

@ -60,7 +60,6 @@ import org.joda.time.DateTimeZone;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
@ -72,7 +71,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 = DateFormatter.forPattern("epoch_millis", Locale.ROOT).toDateMathParser();
private static DateMathParser EPOCH_MILLIS_PARSER = DateFormatter.forPattern("epoch_millis").toDateMathParser();
public static final Map<String, DateTimeUnit> DATE_FIELD_UNITS;

View File

@ -473,7 +473,7 @@ public class JavaJodaTimeDuellingTests extends ESTestCase {
public void testSeveralTimeFormats() {
DateFormatter jodaFormatter = DateFormatter.forPattern("year_month_day||ordinal_date");
DateFormatter javaFormatter = DateFormatters.forPattern("year_month_day||ordinal_date");
DateFormatter javaFormatter = DateFormatter.forPattern("8year_month_day||8ordinal_date");
assertSameDate("2018-12-12", "year_month_day||ordinal_date", jodaFormatter, javaFormatter);
assertSameDate("2018-128", "year_month_day||ordinal_date", jodaFormatter, javaFormatter);
}
@ -488,7 +488,7 @@ public class JavaJodaTimeDuellingTests extends ESTestCase {
}
private void assertSameDate(String input, String format) {
DateFormatter jodaFormatter = Joda.forPattern(format, Locale.ROOT);
DateFormatter jodaFormatter = Joda.forPattern(format);
DateFormatter javaFormatter = DateFormatters.forPattern(format);
assertSameDate(input, format, jodaFormatter, javaFormatter);
@ -512,7 +512,7 @@ public class JavaJodaTimeDuellingTests extends ESTestCase {
}
private void assertJodaParseException(String input, String format, String expectedMessage) {
DateFormatter jodaFormatter = Joda.forPattern(format, Locale.ROOT);
DateFormatter jodaFormatter = Joda.forPattern(format);
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> jodaFormatter.parseJoda(input));
assertThat(e.getMessage(), containsString(expectedMessage));
}

View File

@ -27,7 +27,6 @@ import org.elasticsearch.test.ESTestCase;
import org.joda.time.DateTimeZone;
import java.time.ZoneId;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.LongSupplier;
@ -186,7 +185,7 @@ public class JodaDateMathParserTests extends ESTestCase {
assertDateMathEquals("2014-11-18T09:20", "2014-11-18T08:20:59.999Z", 0, true, DateTimeZone.forID("CET"));
// implicit rounding with explicit timezone in the date format
DateFormatter formatter = DateFormatter.forPattern("YYYY-MM-ddZ");
DateFormatter formatter = DateFormatter.forPattern("yyyy-MM-ddZ");
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);
@ -261,7 +260,7 @@ public class JodaDateMathParserTests extends ESTestCase {
assertDateMathEquals("1418248078000||/m", "2014-12-10T21:47:00.000");
// also check other time units
JodaDateMathParser parser = new JodaDateMathParser(Joda.forPattern("epoch_second", Locale.ROOT));
JodaDateMathParser parser = new JodaDateMathParser(Joda.forPattern("epoch_second"));
long datetime = parser.parse("1418248078", () -> 0);
assertDateEquals(datetime, "1418248078", "2014-12-10T21:47:58.000");
@ -308,7 +307,7 @@ public class JodaDateMathParserTests extends ESTestCase {
}
public void testThatUnixTimestampMayNotHaveTimeZone() {
JodaDateMathParser parser = new JodaDateMathParser(Joda.forPattern("epoch_millis", Locale.ROOT));
JodaDateMathParser parser = new JodaDateMathParser(Joda.forPattern("epoch_millis"));
try {
parser.parse("1234567890123", () -> 42, false, ZoneId.of("CET"));
fail("Expected ElasticsearchParseException");

View File

@ -346,11 +346,11 @@ public class SimpleJodaTests extends ESTestCase {
}
public void testThatEpochParserIsPrinter() {
JodaDateFormatter formatter = Joda.forPattern("epoch_millis", Locale.ROOT);
JodaDateFormatter formatter = Joda.forPattern("epoch_millis");
assertThat(formatter.parser.isPrinter(), is(true));
assertThat(formatter.printer.isPrinter(), is(true));
JodaDateFormatter epochSecondFormatter = Joda.forPattern("epoch_second", Locale.ROOT);
JodaDateFormatter epochSecondFormatter = Joda.forPattern("epoch_second");
assertThat(epochSecondFormatter.parser.isPrinter(), is(true));
assertThat(epochSecondFormatter.printer.isPrinter(), is(true));
}
@ -736,6 +736,23 @@ public class SimpleJodaTests extends ESTestCase {
}
}
public void testDeprecatedFormatSpecifiers() {
Joda.forPattern("CC");
assertWarnings("Use of 'C' (century-of-era) is deprecated and will not be supported in the" +
" next major version of Elasticsearch.");
Joda.forPattern("YYYY");
assertWarnings("Use of 'Y' (year-of-era) will change to 'y' in the" +
" next major version of Elasticsearch. Prefix your date format with '8' to use the new specifier.");
Joda.forPattern("xxxx");
assertWarnings("Use of 'x' (week-based-year) will change" +
" to 'Y' in the next major version of Elasticsearch. Prefix your date format with '8' to use the new specifier.");
// multiple deprecations
Joda.forPattern("CC-YYYY");
assertWarnings("Use of 'C' (century-of-era) is deprecated and will not be supported in the" +
" next major version of Elasticsearch.", "Use of 'Y' (year-of-era) will change to 'y' in the" +
" next major version of Elasticsearch. Prefix your date format with '8' to use the new specifier.");
}
private void assertValidDateFormatParsing(String pattern, String dateToParse) {
assertValidDateFormatParsing(pattern, dateToParse, dateToParse);
}

View File

@ -19,6 +19,7 @@
package org.elasticsearch.common.time;
import org.elasticsearch.common.joda.JodaDateFormatter;
import org.elasticsearch.test.ESTestCase;
import java.time.Instant;
@ -30,6 +31,7 @@ import java.util.Locale;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
@ -192,4 +194,16 @@ public class DateFormattersTests extends ESTestCase {
assertThat(epochMillisFormatter, sameInstance(DateFormatters.forPattern("epoch_millis")));
assertThat(epochMillisFormatter, equalTo(DateFormatters.forPattern("epoch_millis")));
}
public void testForceJava8() {
assertThat(DateFormatter.forPattern("8yyyy-MM-dd"), instanceOf(JavaDateFormatter.class));
// named formats too
assertThat(DateFormatter.forPattern("8date_optional_time"), instanceOf(JavaDateFormatter.class));
// named formats too
DateFormatter formatter = DateFormatter.forPattern("8date_optional_time||ww-MM-dd");
assertThat(formatter, instanceOf(DateFormatters.MergedDateFormatter.class));
DateFormatters.MergedDateFormatter mergedFormatter = (DateFormatters.MergedDateFormatter) formatter;
assertThat(mergedFormatter.formatters.get(0), instanceOf(JavaDateFormatter.class));
assertThat(mergedFormatter.formatters.get(1), instanceOf(JodaDateFormatter.class));
}
}

View File

@ -239,7 +239,7 @@ public class JavaDateMathParserTests extends ESTestCase {
assertDateMathEquals("1418248078000||/m", "2014-12-10T21:47:00.000");
// also check other time units
DateMathParser parser = DateFormatters.forPattern("epoch_second||dateOptionalTime").toDateMathParser();
DateMathParser parser = DateFormatter.forPattern("8epoch_second||8dateOptionalTime").toDateMathParser();
long datetime = parser.parse("1418248078", () -> 0);
assertDateEquals(datetime, "1418248078", "2014-12-10T21:47:58.000");

View File

@ -61,13 +61,13 @@ public class DateFieldTypeTests extends FieldTypeTestCase {
addModifier(new Modifier("format", false) {
@Override
public void modify(MappedFieldType ft) {
((DateFieldType) ft).setDateTimeFormatter(DateFormatter.forPattern("basic_week_date", Locale.ROOT));
((DateFieldType) ft).setDateTimeFormatter(DateFormatter.forPattern("basic_week_date"));
}
});
addModifier(new Modifier("locale", false) {
@Override
public void modify(MappedFieldType ft) {
((DateFieldType) ft).setDateTimeFormatter(DateFormatter.forPattern("date_optional_time", Locale.CANADA));
((DateFieldType) ft).setDateTimeFormatter(DateFormatter.forPattern("date_optional_time").withLocale(Locale.CANADA));
}
});
nowInMillis = randomNonNegativeLong();
@ -144,7 +144,7 @@ public class DateFieldTypeTests extends FieldTypeTestCase {
assertEquals("2015-10-12T15:10:55.000+01:00",
ft.docValueFormat(null, DateTimeZone.forOffsetHours(1)).format(instant));
assertEquals("2015",
createDefaultFieldType().docValueFormat("YYYY", DateTimeZone.UTC).format(instant));
createDefaultFieldType().docValueFormat("yyyy", DateTimeZone.UTC).format(instant));
assertEquals(instant,
ft.docValueFormat(null, DateTimeZone.UTC).parseLong("2015-10-12T14:10:55", false, null));
assertEquals(instant + 999,

View File

@ -62,13 +62,13 @@ public class RangeFieldTypeTests extends FieldTypeTestCase {
addModifier(new Modifier("format", true) {
@Override
public void modify(MappedFieldType ft) {
((RangeFieldType) ft).setDateTimeFormatter(DateFormatter.forPattern("basic_week_date", Locale.ROOT));
((RangeFieldType) ft).setDateTimeFormatter(DateFormatter.forPattern("basic_week_date"));
}
});
addModifier(new Modifier("locale", true) {
@Override
public void modify(MappedFieldType ft) {
((RangeFieldType) ft).setDateTimeFormatter(DateFormatter.forPattern("date_optional_time", Locale.CANADA));
((RangeFieldType) ft).setDateTimeFormatter(DateFormatter.forPattern("date_optional_time").withLocale(Locale.CANADA));
}
});
}

View File

@ -95,7 +95,7 @@ public class RootObjectMapperTests extends ESSingleNodeTestCase {
String mapping = Strings.toString(XContentFactory.jsonBuilder()
.startObject()
.startObject("type")
.field("dynamic_date_formats", Arrays.asList("YYYY-MM-dd"))
.field("dynamic_date_formats", Arrays.asList("yyyy-MM-dd"))
.endObject()
.endObject());
MapperService mapperService = createIndex("test").mapperService();

View File

@ -114,7 +114,7 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
public static final String LICENSE_JOB = "licenseJob";
private static final DateFormatter DATE_FORMATTER = DateFormatter.forPattern("EEEE, MMMMM dd, yyyy", Locale.ROOT);
private static final DateFormatter DATE_FORMATTER = DateFormatter.forPattern("EEEE, MMMMM dd, yyyy");
private static final String ACKNOWLEDGEMENT_HEADER = "This license update requires acknowledgement. To acknowledge the license, " +
"please read the following messages and update the license again, this time with the \"acknowledge=true\" parameter:";