Core: Add java time xcontent serializers (#33120)

This ensures that the java time class exposed by painless have proper
serialization/string representations.

Closes #31853
This commit is contained in:
Alexander Reelsen 2018-08-29 10:00:16 +02:00 committed by GitHub
parent f29f0af7bc
commit 48b388ce82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 203 additions and 5 deletions

View File

@ -95,7 +95,7 @@ setup:
field:
script:
source: "doc.date.get(0)"
- match: { hits.hits.0.fields.field.0: '2017-01-01T12:11:12Z' }
- match: { hits.hits.0.fields.field.0: '2017-01-01T12:11:12.000Z' }
- do:
search:
@ -104,7 +104,7 @@ setup:
field:
script:
source: "doc.date.value"
- match: { hits.hits.0.fields.field.0: '2017-01-01T12:11:12Z' }
- match: { hits.hits.0.fields.field.0: '2017-01-01T12:11:12.000Z' }
---
"geo_point":

View File

@ -48,6 +48,7 @@ import static java.time.temporal.ChronoField.HOUR_OF_DAY;
import static java.time.temporal.ChronoField.MILLI_OF_SECOND;
import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
import static java.time.temporal.ChronoField.NANO_OF_SECOND;
import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;
public class DateFormatters {
@ -81,7 +82,7 @@ public class DateFormatters {
.appendFraction(MILLI_OF_SECOND, 3, 3, true)
.optionalEnd()
.optionalStart()
.appendOffset("+HHmm", "Z")
.appendZoneOrOffsetId()
.optionalEnd()
.optionalEnd()
.toFormatter(Locale.ROOT);
@ -95,7 +96,7 @@ public class DateFormatters {
.appendFraction(MILLI_OF_SECOND, 3, 3, true)
.optionalEnd()
.optionalStart()
.appendZoneOrOffsetId()
.appendOffset("+HHmm", "Z")
.optionalEnd()
.optionalEnd()
.toFormatter(Locale.ROOT);
@ -106,6 +107,40 @@ public class DateFormatters {
private static final CompoundDateTimeFormatter STRICT_DATE_OPTIONAL_TIME =
new CompoundDateTimeFormatter(STRICT_DATE_OPTIONAL_TIME_FORMATTER_1, STRICT_DATE_OPTIONAL_TIME_FORMATTER_2);
private static final DateTimeFormatter STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS_1 = new DateTimeFormatterBuilder()
.append(STRICT_YEAR_MONTH_DAY_FORMATTER)
.optionalStart()
.appendLiteral('T')
.append(STRICT_HOUR_MINUTE_SECOND_FORMATTER)
.optionalStart()
.appendFraction(NANO_OF_SECOND, 3, 9, true)
.optionalEnd()
.optionalStart()
.appendZoneOrOffsetId()
.optionalEnd()
.optionalEnd()
.toFormatter(Locale.ROOT);
private static final DateTimeFormatter STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS_2 = new DateTimeFormatterBuilder()
.append(STRICT_YEAR_MONTH_DAY_FORMATTER)
.optionalStart()
.appendLiteral('T')
.append(STRICT_HOUR_MINUTE_SECOND_FORMATTER)
.optionalStart()
.appendFraction(NANO_OF_SECOND, 3, 9, true)
.optionalEnd()
.optionalStart()
.appendOffset("+HHmm", "Z")
.optionalEnd()
.optionalEnd()
.toFormatter(Locale.ROOT);
/**
* Returns a generic ISO datetime parser where the date is mandatory and the time is optional with nanosecond resolution.
*/
private static final CompoundDateTimeFormatter STRICT_DATE_OPTIONAL_TIME_NANOS =
new CompoundDateTimeFormatter(STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS_1, STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS_2);
/////////////////////////////////////////
//
// BEGIN basic time formatters
@ -1326,6 +1361,8 @@ public class DateFormatters {
return STRICT_DATE_HOUR_MINUTE_SECOND_MILLIS;
} else if ("strictDateOptionalTime".equals(input) || "strict_date_optional_time".equals(input)) {
return STRICT_DATE_OPTIONAL_TIME;
} else if ("strictDateOptionalTimeNanos".equals(input) || "strict_date_optional_time_nanos".equals(input)) {
return STRICT_DATE_OPTIONAL_TIME_NANOS;
} else if ("strictDateTime".equals(input) || "strict_date_time".equals(input)) {
return STRICT_DATE_TIME;
} else if ("strictDateTimeNoMillis".equals(input) || "strict_date_time_no_millis".equals(input)) {

View File

@ -21,6 +21,8 @@ package org.elasticsearch.common.xcontent;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.time.CompoundDateTimeFormatter;
import org.elasticsearch.common.time.DateFormatters;
import org.elasticsearch.common.unit.ByteSizeValue;
import org.elasticsearch.common.unit.TimeValue;
import org.joda.time.DateTime;
@ -33,6 +35,19 @@ import org.joda.time.format.ISODateTimeFormat;
import org.joda.time.tz.CachedDateTimeZone;
import org.joda.time.tz.FixedDateTimeZone;
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
import java.time.MonthDay;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.Period;
import java.time.Year;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
@ -49,6 +64,9 @@ import java.util.function.Function;
public class XContentElasticsearchExtension implements XContentBuilderExtension {
public static final DateTimeFormatter DEFAULT_DATE_PRINTER = ISODateTimeFormat.dateTime().withZone(DateTimeZone.UTC);
public static final CompoundDateTimeFormatter DEFAULT_FORMATTER = DateFormatters.forPattern("strict_date_optional_time_nanos");
public static final CompoundDateTimeFormatter LOCAL_TIME_FORMATTER = DateFormatters.forPattern("HH:mm:ss.SSS");
public static final CompoundDateTimeFormatter OFFSET_TIME_FORMATTER = DateFormatters.forPattern("HH:mm:ss.SSSZZZZZ");
@Override
public Map<Class<?>, XContentBuilder.Writer> getXContentWriters() {
@ -62,6 +80,19 @@ public class XContentElasticsearchExtension implements XContentBuilderExtension
writers.put(MutableDateTime.class, XContentBuilder::timeValue);
writers.put(DateTime.class, XContentBuilder::timeValue);
writers.put(TimeValue.class, (b, v) -> b.value(v.toString()));
writers.put(ZonedDateTime.class, XContentBuilder::timeValue);
writers.put(OffsetDateTime.class, XContentBuilder::timeValue);
writers.put(OffsetTime.class, XContentBuilder::timeValue);
writers.put(java.time.Instant.class, XContentBuilder::timeValue);
writers.put(LocalDateTime.class, XContentBuilder::timeValue);
writers.put(LocalDate.class, XContentBuilder::timeValue);
writers.put(LocalTime.class, XContentBuilder::timeValue);
writers.put(DayOfWeek.class, (b, v) -> b.value(v.toString()));
writers.put(Month.class, (b, v) -> b.value(v.toString()));
writers.put(MonthDay.class, (b, v) -> b.value(v.toString()));
writers.put(Year.class, (b, v) -> b.value(v.toString()));
writers.put(Duration.class, (b, v) -> b.value(v.toString()));
writers.put(Period.class, (b, v) -> b.value(v.toString()));
writers.put(BytesReference.class, (b, v) -> {
if (v == null) {
@ -102,6 +133,14 @@ public class XContentElasticsearchExtension implements XContentBuilderExtension
transformers.put(Calendar.class, d -> DEFAULT_DATE_PRINTER.print(((Calendar) d).getTimeInMillis()));
transformers.put(GregorianCalendar.class, d -> DEFAULT_DATE_PRINTER.print(((Calendar) d).getTimeInMillis()));
transformers.put(Instant.class, d -> DEFAULT_DATE_PRINTER.print((Instant) d));
transformers.put(ZonedDateTime.class, d -> DEFAULT_FORMATTER.format((ZonedDateTime) d));
transformers.put(OffsetDateTime.class, d -> DEFAULT_FORMATTER.format((OffsetDateTime) d));
transformers.put(OffsetTime.class, d -> OFFSET_TIME_FORMATTER.format((OffsetTime) d));
transformers.put(LocalDateTime.class, d -> DEFAULT_FORMATTER.format((LocalDateTime) d));
transformers.put(java.time.Instant.class,
d -> DEFAULT_FORMATTER.format(ZonedDateTime.ofInstant((java.time.Instant) d, ZoneOffset.UTC)));
transformers.put(LocalDate.class, d -> ((LocalDate) d).toString());
transformers.put(LocalTime.class, d -> LOCAL_TIME_FORMATTER.format((LocalTime) d));
return transformers;
}
}

View File

@ -22,7 +22,6 @@ package org.elasticsearch.common.xcontent;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParseException;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.Constants;
import org.elasticsearch.cluster.metadata.IndexMetaData;
@ -51,6 +50,19 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Path;
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
import java.time.MonthDay;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.Period;
import java.time.Year;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
@ -459,6 +471,116 @@ public abstract class BaseXContentTestCase extends ESTestCase {
.endObject());
}
public void testJavaTime() throws Exception {
final ZonedDateTime d1 = ZonedDateTime.of(2016, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
// ZonedDateTime
assertResult("{'date':null}", () -> builder().startObject().timeField("date", (ZonedDateTime) null).endObject());
assertResult("{'date':null}", () -> builder().startObject().field("date").timeValue((ZonedDateTime) null).endObject());
assertResult("{'date':null}", () -> builder().startObject().field("date", (ZonedDateTime) null).endObject());
assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", () -> builder().startObject().timeField("d1", d1).endObject());
assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", () -> builder().startObject().field("d1").timeValue(d1).endObject());
assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", () -> builder().startObject().field("d1", d1).endObject());
// Instant
assertResult("{'date':null}", () -> builder().startObject().timeField("date", (java.time.Instant) null).endObject());
assertResult("{'date':null}", () -> builder().startObject().field("date").timeValue((java.time.Instant) null).endObject());
assertResult("{'date':null}", () -> builder().startObject().field("date", (java.time.Instant) null).endObject());
assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", () -> builder().startObject().timeField("d1", d1.toInstant()).endObject());
assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", () -> builder().startObject().field("d1").timeValue(d1.toInstant()).endObject());
assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", () -> builder().startObject().field("d1", d1.toInstant()).endObject());
// LocalDateTime (no time zone)
assertResult("{'date':null}", () -> builder().startObject().timeField("date", (LocalDateTime) null).endObject());
assertResult("{'date':null}", () -> builder().startObject().field("date").timeValue((LocalDateTime) null).endObject());
assertResult("{'date':null}", () -> builder().startObject().field("date", (LocalDateTime) null).endObject());
assertResult("{'d1':'2016-01-01T00:00:00.000'}",
() -> builder().startObject().timeField("d1", d1.toLocalDateTime()).endObject());
assertResult("{'d1':'2016-01-01T00:00:00.000'}",
() -> builder().startObject().field("d1").timeValue(d1.toLocalDateTime()).endObject());
assertResult("{'d1':'2016-01-01T00:00:00.000'}", () -> builder().startObject().field("d1", d1.toLocalDateTime()).endObject());
// LocalDate (no time, no time zone)
assertResult("{'date':null}", () -> builder().startObject().timeField("date", (LocalDate) null).endObject());
assertResult("{'date':null}", () -> builder().startObject().field("date").timeValue((LocalDate) null).endObject());
assertResult("{'date':null}", () -> builder().startObject().field("date", (LocalDate) null).endObject());
assertResult("{'d1':'2016-01-01'}", () -> builder().startObject().timeField("d1", d1.toLocalDate()).endObject());
assertResult("{'d1':'2016-01-01'}", () -> builder().startObject().field("d1").timeValue(d1.toLocalDate()).endObject());
assertResult("{'d1':'2016-01-01'}", () -> builder().startObject().field("d1", d1.toLocalDate()).endObject());
// LocalTime (no date, no time zone)
assertResult("{'date':null}", () -> builder().startObject().timeField("date", (LocalTime) null).endObject());
assertResult("{'date':null}", () -> builder().startObject().field("date").timeValue((LocalTime) null).endObject());
assertResult("{'date':null}", () -> builder().startObject().field("date", (LocalTime) null).endObject());
assertResult("{'d1':'00:00:00.000'}", () -> builder().startObject().timeField("d1", d1.toLocalTime()).endObject());
assertResult("{'d1':'00:00:00.000'}", () -> builder().startObject().field("d1").timeValue(d1.toLocalTime()).endObject());
assertResult("{'d1':'00:00:00.000'}", () -> builder().startObject().field("d1", d1.toLocalTime()).endObject());
final ZonedDateTime d2 = ZonedDateTime.of(2016, 1, 1, 7, 59, 23, 123_000_000, ZoneOffset.UTC);
assertResult("{'d1':'07:59:23.123'}", () -> builder().startObject().timeField("d1", d2.toLocalTime()).endObject());
assertResult("{'d1':'07:59:23.123'}", () -> builder().startObject().field("d1").timeValue(d2.toLocalTime()).endObject());
assertResult("{'d1':'07:59:23.123'}", () -> builder().startObject().field("d1", d2.toLocalTime()).endObject());
// OffsetDateTime
assertResult("{'date':null}", () -> builder().startObject().timeField("date", (OffsetDateTime) null).endObject());
assertResult("{'date':null}", () -> builder().startObject().field("date").timeValue((OffsetDateTime) null).endObject());
assertResult("{'date':null}", () -> builder().startObject().field("date", (OffsetDateTime) null).endObject());
assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", () -> builder().startObject().field("d1", d1.toOffsetDateTime()).endObject());
assertResult("{'d1':'2016-01-01T00:00:00.000Z'}",
() -> builder().startObject().timeField("d1", d1.toOffsetDateTime()).endObject());
assertResult("{'d1':'2016-01-01T00:00:00.000Z'}",
() -> builder().startObject().field("d1").timeValue(d1.toOffsetDateTime()).endObject());
// also test with a date that has a real offset
OffsetDateTime offsetDateTime = d1.withZoneSameLocal(ZoneOffset.ofHours(5)).toOffsetDateTime();
assertResult("{'d1':'2016-01-01T00:00:00.000+05:00'}", () -> builder().startObject().field("d1", offsetDateTime).endObject());
assertResult("{'d1':'2016-01-01T00:00:00.000+05:00'}", () -> builder().startObject().timeField("d1", offsetDateTime).endObject());
assertResult("{'d1':'2016-01-01T00:00:00.000+05:00'}",
() -> builder().startObject().field("d1").timeValue(offsetDateTime).endObject());
// OffsetTime
assertResult("{'date':null}", () -> builder().startObject().timeField("date", (OffsetTime) null).endObject());
assertResult("{'date':null}", () -> builder().startObject().field("date").timeValue((OffsetTime) null).endObject());
assertResult("{'date':null}", () -> builder().startObject().field("date", (OffsetTime) null).endObject());
final OffsetTime offsetTime = d2.toOffsetDateTime().toOffsetTime();
assertResult("{'o':'07:59:23.123Z'}", () -> builder().startObject().timeField("o", offsetTime).endObject());
assertResult("{'o':'07:59:23.123Z'}", () -> builder().startObject().field("o").timeValue(offsetTime).endObject());
assertResult("{'o':'07:59:23.123Z'}", () -> builder().startObject().field("o", offsetTime).endObject());
// also test with a date that has a real offset
final OffsetTime zonedOffsetTime = offsetTime.withOffsetSameLocal(ZoneOffset.ofHours(5));
assertResult("{'o':'07:59:23.123+05:00'}", () -> builder().startObject().timeField("o", zonedOffsetTime).endObject());
assertResult("{'o':'07:59:23.123+05:00'}", () -> builder().startObject().field("o").timeValue(zonedOffsetTime).endObject());
assertResult("{'o':'07:59:23.123+05:00'}", () -> builder().startObject().field("o", zonedOffsetTime).endObject());
// DayOfWeek enum, not a real time value, but might be used in scripts
assertResult("{'dayOfWeek':null}", () -> builder().startObject().field("dayOfWeek", (DayOfWeek) null).endObject());
DayOfWeek dayOfWeek = randomFrom(DayOfWeek.values());
assertResult("{'dayOfWeek':'" + dayOfWeek + "'}", () -> builder().startObject().field("dayOfWeek", dayOfWeek).endObject());
// Month
Month month = randomFrom(Month.values());
assertResult("{'m':null}", () -> builder().startObject().field("m", (Month) null).endObject());
assertResult("{'m':'" + month + "'}", () -> builder().startObject().field("m", month).endObject());
// MonthDay
MonthDay monthDay = MonthDay.of(month, randomIntBetween(1, 28));
assertResult("{'m':null}", () -> builder().startObject().field("m", (MonthDay) null).endObject());
assertResult("{'m':'" + monthDay + "'}", () -> builder().startObject().field("m", monthDay).endObject());
// Year
Year year = Year.of(randomIntBetween(0, 2300));
assertResult("{'y':null}", () -> builder().startObject().field("y", (Year) null).endObject());
assertResult("{'y':'" + year + "'}", () -> builder().startObject().field("y", year).endObject());
// Duration
Duration duration = Duration.ofSeconds(randomInt(100000));
assertResult("{'d':null}", () -> builder().startObject().field("d", (Duration) null).endObject());
assertResult("{'d':'" + duration + "'}", () -> builder().startObject().field("d", duration).endObject());
// Period
Period period = Period.ofDays(randomInt(1000));
assertResult("{'p':null}", () -> builder().startObject().field("p", (Period) null).endObject());
assertResult("{'p':'" + period + "'}", () -> builder().startObject().field("p", period).endObject());
}
public void testGeoPoint() throws Exception {
assertResult("{'geo':null}", () -> builder().startObject().field("geo", (GeoPoint) null).endObject());
assertResult("{'geo':{'lat':52.4267578125,'lon':13.271484375}}", () -> builder()