Skip to content

Commit

Permalink
Core: Add java time xcontent serializers (#33120)
Browse files Browse the repository at this point in the history
This ensures that the java time class exposed by painless have proper
serialization/string representations.

Closes #31853
  • Loading branch information
spinscale authored Aug 29, 2018
1 parent f29f0af commit 48b388c
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
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 {
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -1326,6 +1361,8 @@ public static CompoundDateTimeFormatter forPattern(String input, Locale locale)
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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

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;
Expand All @@ -33,6 +35,19 @@
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;
Expand All @@ -49,6 +64,9 @@
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() {
Expand All @@ -62,6 +80,19 @@ public Map<Class<?>, XContentBuilder.Writer> getXContentWriters() {
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) {
Expand Down Expand Up @@ -102,6 +133,14 @@ public Map<Class<?>, Function<Object, Object>> getDateTransformers() {
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
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;
Expand Down Expand Up @@ -51,6 +50,19 @@
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;
Expand Down Expand Up @@ -459,6 +471,116 @@ public void testCalendar() throws Exception {
.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()
Expand Down

0 comments on commit 48b388c

Please sign in to comment.