Skip to content

Commit 48b388c

Browse files
authored
Core: Add java time xcontent serializers (#33120)
This ensures that the java time class exposed by painless have proper serialization/string representations. Closes #31853
1 parent f29f0af commit 48b388c

File tree

4 files changed

+203
-5
lines changed

4 files changed

+203
-5
lines changed

modules/lang-painless/src/test/resources/rest-api-spec/test/painless/50_script_doc_values.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ setup:
9595
field:
9696
script:
9797
source: "doc.date.get(0)"
98-
- match: { hits.hits.0.fields.field.0: '2017-01-01T12:11:12Z' }
98+
- match: { hits.hits.0.fields.field.0: '2017-01-01T12:11:12.000Z' }
9999

100100
- do:
101101
search:
@@ -104,7 +104,7 @@ setup:
104104
field:
105105
script:
106106
source: "doc.date.value"
107-
- match: { hits.hits.0.fields.field.0: '2017-01-01T12:11:12Z' }
107+
- match: { hits.hits.0.fields.field.0: '2017-01-01T12:11:12.000Z' }
108108

109109
---
110110
"geo_point":

server/src/main/java/org/elasticsearch/common/time/DateFormatters.java

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import static java.time.temporal.ChronoField.MILLI_OF_SECOND;
4949
import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
5050
import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
51+
import static java.time.temporal.ChronoField.NANO_OF_SECOND;
5152
import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;
5253

5354
public class DateFormatters {
@@ -81,7 +82,7 @@ public class DateFormatters {
8182
.appendFraction(MILLI_OF_SECOND, 3, 3, true)
8283
.optionalEnd()
8384
.optionalStart()
84-
.appendOffset("+HHmm", "Z")
85+
.appendZoneOrOffsetId()
8586
.optionalEnd()
8687
.optionalEnd()
8788
.toFormatter(Locale.ROOT);
@@ -95,7 +96,7 @@ public class DateFormatters {
9596
.appendFraction(MILLI_OF_SECOND, 3, 3, true)
9697
.optionalEnd()
9798
.optionalStart()
98-
.appendZoneOrOffsetId()
99+
.appendOffset("+HHmm", "Z")
99100
.optionalEnd()
100101
.optionalEnd()
101102
.toFormatter(Locale.ROOT);
@@ -106,6 +107,40 @@ public class DateFormatters {
106107
private static final CompoundDateTimeFormatter STRICT_DATE_OPTIONAL_TIME =
107108
new CompoundDateTimeFormatter(STRICT_DATE_OPTIONAL_TIME_FORMATTER_1, STRICT_DATE_OPTIONAL_TIME_FORMATTER_2);
108109

110+
private static final DateTimeFormatter STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS_1 = new DateTimeFormatterBuilder()
111+
.append(STRICT_YEAR_MONTH_DAY_FORMATTER)
112+
.optionalStart()
113+
.appendLiteral('T')
114+
.append(STRICT_HOUR_MINUTE_SECOND_FORMATTER)
115+
.optionalStart()
116+
.appendFraction(NANO_OF_SECOND, 3, 9, true)
117+
.optionalEnd()
118+
.optionalStart()
119+
.appendZoneOrOffsetId()
120+
.optionalEnd()
121+
.optionalEnd()
122+
.toFormatter(Locale.ROOT);
123+
124+
private static final DateTimeFormatter STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS_2 = new DateTimeFormatterBuilder()
125+
.append(STRICT_YEAR_MONTH_DAY_FORMATTER)
126+
.optionalStart()
127+
.appendLiteral('T')
128+
.append(STRICT_HOUR_MINUTE_SECOND_FORMATTER)
129+
.optionalStart()
130+
.appendFraction(NANO_OF_SECOND, 3, 9, true)
131+
.optionalEnd()
132+
.optionalStart()
133+
.appendOffset("+HHmm", "Z")
134+
.optionalEnd()
135+
.optionalEnd()
136+
.toFormatter(Locale.ROOT);
137+
138+
/**
139+
* Returns a generic ISO datetime parser where the date is mandatory and the time is optional with nanosecond resolution.
140+
*/
141+
private static final CompoundDateTimeFormatter STRICT_DATE_OPTIONAL_TIME_NANOS =
142+
new CompoundDateTimeFormatter(STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS_1, STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS_2);
143+
109144
/////////////////////////////////////////
110145
//
111146
// BEGIN basic time formatters
@@ -1326,6 +1361,8 @@ public static CompoundDateTimeFormatter forPattern(String input, Locale locale)
13261361
return STRICT_DATE_HOUR_MINUTE_SECOND_MILLIS;
13271362
} else if ("strictDateOptionalTime".equals(input) || "strict_date_optional_time".equals(input)) {
13281363
return STRICT_DATE_OPTIONAL_TIME;
1364+
} else if ("strictDateOptionalTimeNanos".equals(input) || "strict_date_optional_time_nanos".equals(input)) {
1365+
return STRICT_DATE_OPTIONAL_TIME_NANOS;
13291366
} else if ("strictDateTime".equals(input) || "strict_date_time".equals(input)) {
13301367
return STRICT_DATE_TIME;
13311368
} else if ("strictDateTimeNoMillis".equals(input) || "strict_date_time_no_millis".equals(input)) {

server/src/main/java/org/elasticsearch/common/xcontent/XContentElasticsearchExtension.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
import org.apache.lucene.util.BytesRef;
2323
import org.elasticsearch.common.bytes.BytesReference;
24+
import org.elasticsearch.common.time.CompoundDateTimeFormatter;
25+
import org.elasticsearch.common.time.DateFormatters;
2426
import org.elasticsearch.common.unit.ByteSizeValue;
2527
import org.elasticsearch.common.unit.TimeValue;
2628
import org.joda.time.DateTime;
@@ -33,6 +35,19 @@
3335
import org.joda.time.tz.CachedDateTimeZone;
3436
import org.joda.time.tz.FixedDateTimeZone;
3537

38+
import java.time.DayOfWeek;
39+
import java.time.Duration;
40+
import java.time.LocalDate;
41+
import java.time.LocalDateTime;
42+
import java.time.LocalTime;
43+
import java.time.Month;
44+
import java.time.MonthDay;
45+
import java.time.OffsetDateTime;
46+
import java.time.OffsetTime;
47+
import java.time.Period;
48+
import java.time.Year;
49+
import java.time.ZoneOffset;
50+
import java.time.ZonedDateTime;
3651
import java.util.Calendar;
3752
import java.util.Date;
3853
import java.util.GregorianCalendar;
@@ -49,6 +64,9 @@
4964
public class XContentElasticsearchExtension implements XContentBuilderExtension {
5065

5166
public static final DateTimeFormatter DEFAULT_DATE_PRINTER = ISODateTimeFormat.dateTime().withZone(DateTimeZone.UTC);
67+
public static final CompoundDateTimeFormatter DEFAULT_FORMATTER = DateFormatters.forPattern("strict_date_optional_time_nanos");
68+
public static final CompoundDateTimeFormatter LOCAL_TIME_FORMATTER = DateFormatters.forPattern("HH:mm:ss.SSS");
69+
public static final CompoundDateTimeFormatter OFFSET_TIME_FORMATTER = DateFormatters.forPattern("HH:mm:ss.SSSZZZZZ");
5270

5371
@Override
5472
public Map<Class<?>, XContentBuilder.Writer> getXContentWriters() {
@@ -62,6 +80,19 @@ public Map<Class<?>, XContentBuilder.Writer> getXContentWriters() {
6280
writers.put(MutableDateTime.class, XContentBuilder::timeValue);
6381
writers.put(DateTime.class, XContentBuilder::timeValue);
6482
writers.put(TimeValue.class, (b, v) -> b.value(v.toString()));
83+
writers.put(ZonedDateTime.class, XContentBuilder::timeValue);
84+
writers.put(OffsetDateTime.class, XContentBuilder::timeValue);
85+
writers.put(OffsetTime.class, XContentBuilder::timeValue);
86+
writers.put(java.time.Instant.class, XContentBuilder::timeValue);
87+
writers.put(LocalDateTime.class, XContentBuilder::timeValue);
88+
writers.put(LocalDate.class, XContentBuilder::timeValue);
89+
writers.put(LocalTime.class, XContentBuilder::timeValue);
90+
writers.put(DayOfWeek.class, (b, v) -> b.value(v.toString()));
91+
writers.put(Month.class, (b, v) -> b.value(v.toString()));
92+
writers.put(MonthDay.class, (b, v) -> b.value(v.toString()));
93+
writers.put(Year.class, (b, v) -> b.value(v.toString()));
94+
writers.put(Duration.class, (b, v) -> b.value(v.toString()));
95+
writers.put(Period.class, (b, v) -> b.value(v.toString()));
6596

6697
writers.put(BytesReference.class, (b, v) -> {
6798
if (v == null) {
@@ -102,6 +133,14 @@ public Map<Class<?>, Function<Object, Object>> getDateTransformers() {
102133
transformers.put(Calendar.class, d -> DEFAULT_DATE_PRINTER.print(((Calendar) d).getTimeInMillis()));
103134
transformers.put(GregorianCalendar.class, d -> DEFAULT_DATE_PRINTER.print(((Calendar) d).getTimeInMillis()));
104135
transformers.put(Instant.class, d -> DEFAULT_DATE_PRINTER.print((Instant) d));
136+
transformers.put(ZonedDateTime.class, d -> DEFAULT_FORMATTER.format((ZonedDateTime) d));
137+
transformers.put(OffsetDateTime.class, d -> DEFAULT_FORMATTER.format((OffsetDateTime) d));
138+
transformers.put(OffsetTime.class, d -> OFFSET_TIME_FORMATTER.format((OffsetTime) d));
139+
transformers.put(LocalDateTime.class, d -> DEFAULT_FORMATTER.format((LocalDateTime) d));
140+
transformers.put(java.time.Instant.class,
141+
d -> DEFAULT_FORMATTER.format(ZonedDateTime.ofInstant((java.time.Instant) d, ZoneOffset.UTC)));
142+
transformers.put(LocalDate.class, d -> ((LocalDate) d).toString());
143+
transformers.put(LocalTime.class, d -> LOCAL_TIME_FORMATTER.format((LocalTime) d));
105144
return transformers;
106145
}
107146
}

server/src/test/java/org/elasticsearch/common/xcontent/BaseXContentTestCase.java

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import com.fasterxml.jackson.core.JsonGenerationException;
2323
import com.fasterxml.jackson.core.JsonGenerator;
2424
import com.fasterxml.jackson.core.JsonParseException;
25-
2625
import org.apache.lucene.util.BytesRef;
2726
import org.apache.lucene.util.Constants;
2827
import org.elasticsearch.cluster.metadata.IndexMetaData;
@@ -51,6 +50,19 @@
5150
import java.io.IOException;
5251
import java.math.BigInteger;
5352
import java.nio.file.Path;
53+
import java.time.DayOfWeek;
54+
import java.time.Duration;
55+
import java.time.LocalDate;
56+
import java.time.LocalDateTime;
57+
import java.time.LocalTime;
58+
import java.time.Month;
59+
import java.time.MonthDay;
60+
import java.time.OffsetDateTime;
61+
import java.time.OffsetTime;
62+
import java.time.Period;
63+
import java.time.Year;
64+
import java.time.ZoneOffset;
65+
import java.time.ZonedDateTime;
5466
import java.util.ArrayList;
5567
import java.util.Arrays;
5668
import java.util.Calendar;
@@ -459,6 +471,116 @@ public void testCalendar() throws Exception {
459471
.endObject());
460472
}
461473

474+
public void testJavaTime() throws Exception {
475+
final ZonedDateTime d1 = ZonedDateTime.of(2016, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
476+
477+
// ZonedDateTime
478+
assertResult("{'date':null}", () -> builder().startObject().timeField("date", (ZonedDateTime) null).endObject());
479+
assertResult("{'date':null}", () -> builder().startObject().field("date").timeValue((ZonedDateTime) null).endObject());
480+
assertResult("{'date':null}", () -> builder().startObject().field("date", (ZonedDateTime) null).endObject());
481+
assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", () -> builder().startObject().timeField("d1", d1).endObject());
482+
assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", () -> builder().startObject().field("d1").timeValue(d1).endObject());
483+
assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", () -> builder().startObject().field("d1", d1).endObject());
484+
485+
// Instant
486+
assertResult("{'date':null}", () -> builder().startObject().timeField("date", (java.time.Instant) null).endObject());
487+
assertResult("{'date':null}", () -> builder().startObject().field("date").timeValue((java.time.Instant) null).endObject());
488+
assertResult("{'date':null}", () -> builder().startObject().field("date", (java.time.Instant) null).endObject());
489+
assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", () -> builder().startObject().timeField("d1", d1.toInstant()).endObject());
490+
assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", () -> builder().startObject().field("d1").timeValue(d1.toInstant()).endObject());
491+
assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", () -> builder().startObject().field("d1", d1.toInstant()).endObject());
492+
493+
// LocalDateTime (no time zone)
494+
assertResult("{'date':null}", () -> builder().startObject().timeField("date", (LocalDateTime) null).endObject());
495+
assertResult("{'date':null}", () -> builder().startObject().field("date").timeValue((LocalDateTime) null).endObject());
496+
assertResult("{'date':null}", () -> builder().startObject().field("date", (LocalDateTime) null).endObject());
497+
assertResult("{'d1':'2016-01-01T00:00:00.000'}",
498+
() -> builder().startObject().timeField("d1", d1.toLocalDateTime()).endObject());
499+
assertResult("{'d1':'2016-01-01T00:00:00.000'}",
500+
() -> builder().startObject().field("d1").timeValue(d1.toLocalDateTime()).endObject());
501+
assertResult("{'d1':'2016-01-01T00:00:00.000'}", () -> builder().startObject().field("d1", d1.toLocalDateTime()).endObject());
502+
503+
// LocalDate (no time, no time zone)
504+
assertResult("{'date':null}", () -> builder().startObject().timeField("date", (LocalDate) null).endObject());
505+
assertResult("{'date':null}", () -> builder().startObject().field("date").timeValue((LocalDate) null).endObject());
506+
assertResult("{'date':null}", () -> builder().startObject().field("date", (LocalDate) null).endObject());
507+
assertResult("{'d1':'2016-01-01'}", () -> builder().startObject().timeField("d1", d1.toLocalDate()).endObject());
508+
assertResult("{'d1':'2016-01-01'}", () -> builder().startObject().field("d1").timeValue(d1.toLocalDate()).endObject());
509+
assertResult("{'d1':'2016-01-01'}", () -> builder().startObject().field("d1", d1.toLocalDate()).endObject());
510+
511+
// LocalTime (no date, no time zone)
512+
assertResult("{'date':null}", () -> builder().startObject().timeField("date", (LocalTime) null).endObject());
513+
assertResult("{'date':null}", () -> builder().startObject().field("date").timeValue((LocalTime) null).endObject());
514+
assertResult("{'date':null}", () -> builder().startObject().field("date", (LocalTime) null).endObject());
515+
assertResult("{'d1':'00:00:00.000'}", () -> builder().startObject().timeField("d1", d1.toLocalTime()).endObject());
516+
assertResult("{'d1':'00:00:00.000'}", () -> builder().startObject().field("d1").timeValue(d1.toLocalTime()).endObject());
517+
assertResult("{'d1':'00:00:00.000'}", () -> builder().startObject().field("d1", d1.toLocalTime()).endObject());
518+
final ZonedDateTime d2 = ZonedDateTime.of(2016, 1, 1, 7, 59, 23, 123_000_000, ZoneOffset.UTC);
519+
assertResult("{'d1':'07:59:23.123'}", () -> builder().startObject().timeField("d1", d2.toLocalTime()).endObject());
520+
assertResult("{'d1':'07:59:23.123'}", () -> builder().startObject().field("d1").timeValue(d2.toLocalTime()).endObject());
521+
assertResult("{'d1':'07:59:23.123'}", () -> builder().startObject().field("d1", d2.toLocalTime()).endObject());
522+
523+
// OffsetDateTime
524+
assertResult("{'date':null}", () -> builder().startObject().timeField("date", (OffsetDateTime) null).endObject());
525+
assertResult("{'date':null}", () -> builder().startObject().field("date").timeValue((OffsetDateTime) null).endObject());
526+
assertResult("{'date':null}", () -> builder().startObject().field("date", (OffsetDateTime) null).endObject());
527+
assertResult("{'d1':'2016-01-01T00:00:00.000Z'}", () -> builder().startObject().field("d1", d1.toOffsetDateTime()).endObject());
528+
assertResult("{'d1':'2016-01-01T00:00:00.000Z'}",
529+
() -> builder().startObject().timeField("d1", d1.toOffsetDateTime()).endObject());
530+
assertResult("{'d1':'2016-01-01T00:00:00.000Z'}",
531+
() -> builder().startObject().field("d1").timeValue(d1.toOffsetDateTime()).endObject());
532+
// also test with a date that has a real offset
533+
OffsetDateTime offsetDateTime = d1.withZoneSameLocal(ZoneOffset.ofHours(5)).toOffsetDateTime();
534+
assertResult("{'d1':'2016-01-01T00:00:00.000+05:00'}", () -> builder().startObject().field("d1", offsetDateTime).endObject());
535+
assertResult("{'d1':'2016-01-01T00:00:00.000+05:00'}", () -> builder().startObject().timeField("d1", offsetDateTime).endObject());
536+
assertResult("{'d1':'2016-01-01T00:00:00.000+05:00'}",
537+
() -> builder().startObject().field("d1").timeValue(offsetDateTime).endObject());
538+
539+
// OffsetTime
540+
assertResult("{'date':null}", () -> builder().startObject().timeField("date", (OffsetTime) null).endObject());
541+
assertResult("{'date':null}", () -> builder().startObject().field("date").timeValue((OffsetTime) null).endObject());
542+
assertResult("{'date':null}", () -> builder().startObject().field("date", (OffsetTime) null).endObject());
543+
final OffsetTime offsetTime = d2.toOffsetDateTime().toOffsetTime();
544+
assertResult("{'o':'07:59:23.123Z'}", () -> builder().startObject().timeField("o", offsetTime).endObject());
545+
assertResult("{'o':'07:59:23.123Z'}", () -> builder().startObject().field("o").timeValue(offsetTime).endObject());
546+
assertResult("{'o':'07:59:23.123Z'}", () -> builder().startObject().field("o", offsetTime).endObject());
547+
// also test with a date that has a real offset
548+
final OffsetTime zonedOffsetTime = offsetTime.withOffsetSameLocal(ZoneOffset.ofHours(5));
549+
assertResult("{'o':'07:59:23.123+05:00'}", () -> builder().startObject().timeField("o", zonedOffsetTime).endObject());
550+
assertResult("{'o':'07:59:23.123+05:00'}", () -> builder().startObject().field("o").timeValue(zonedOffsetTime).endObject());
551+
assertResult("{'o':'07:59:23.123+05:00'}", () -> builder().startObject().field("o", zonedOffsetTime).endObject());
552+
553+
// DayOfWeek enum, not a real time value, but might be used in scripts
554+
assertResult("{'dayOfWeek':null}", () -> builder().startObject().field("dayOfWeek", (DayOfWeek) null).endObject());
555+
DayOfWeek dayOfWeek = randomFrom(DayOfWeek.values());
556+
assertResult("{'dayOfWeek':'" + dayOfWeek + "'}", () -> builder().startObject().field("dayOfWeek", dayOfWeek).endObject());
557+
558+
// Month
559+
Month month = randomFrom(Month.values());
560+
assertResult("{'m':null}", () -> builder().startObject().field("m", (Month) null).endObject());
561+
assertResult("{'m':'" + month + "'}", () -> builder().startObject().field("m", month).endObject());
562+
563+
// MonthDay
564+
MonthDay monthDay = MonthDay.of(month, randomIntBetween(1, 28));
565+
assertResult("{'m':null}", () -> builder().startObject().field("m", (MonthDay) null).endObject());
566+
assertResult("{'m':'" + monthDay + "'}", () -> builder().startObject().field("m", monthDay).endObject());
567+
568+
// Year
569+
Year year = Year.of(randomIntBetween(0, 2300));
570+
assertResult("{'y':null}", () -> builder().startObject().field("y", (Year) null).endObject());
571+
assertResult("{'y':'" + year + "'}", () -> builder().startObject().field("y", year).endObject());
572+
573+
// Duration
574+
Duration duration = Duration.ofSeconds(randomInt(100000));
575+
assertResult("{'d':null}", () -> builder().startObject().field("d", (Duration) null).endObject());
576+
assertResult("{'d':'" + duration + "'}", () -> builder().startObject().field("d", duration).endObject());
577+
578+
// Period
579+
Period period = Period.ofDays(randomInt(1000));
580+
assertResult("{'p':null}", () -> builder().startObject().field("p", (Period) null).endObject());
581+
assertResult("{'p':'" + period + "'}", () -> builder().startObject().field("p", period).endObject());
582+
}
583+
462584
public void testGeoPoint() throws Exception {
463585
assertResult("{'geo':null}", () -> builder().startObject().field("geo", (GeoPoint) null).endObject());
464586
assertResult("{'geo':{'lat':52.4267578125,'lon':13.271484375}}", () -> builder()

0 commit comments

Comments
 (0)