Skip to content

Commit c4f4378

Browse files
authored
Core: Rework multi date formatter merging (#36447)
This commit moves the MergedDateFormatter to a package private class and reworks joda DateFormatter instances to use that instead of a single DateTimeFormatter with multiple parsers. This will allow the java and joda multi formats to share the same format parsing method in a followup.
1 parent 1bb6f84 commit c4f4378

File tree

30 files changed

+217
-238
lines changed

30 files changed

+217
-238
lines changed

server/src/main/java/org/elasticsearch/common/joda/Joda.java

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,6 @@
4848

4949
public class Joda {
5050

51-
public static JodaDateFormatter forPattern(String input) {
52-
return forPattern(input, Locale.ROOT);
53-
}
54-
5551
/**
5652
* Parses a joda based pattern, including some named ones (similar to the built in Joda ISO ones).
5753
*/
@@ -231,27 +227,7 @@ public static JodaDateFormatter forPattern(String input, Locale locale) {
231227
formatter = StrictISODateTimeFormat.yearMonth();
232228
} else if ("strictYearMonthDay".equals(input) || "strict_year_month_day".equals(input)) {
233229
formatter = StrictISODateTimeFormat.yearMonthDay();
234-
} else if (Strings.hasLength(input) && input.contains("||")) {
235-
String[] formats = Strings.delimitedListToStringArray(input, "||");
236-
DateTimeParser[] parsers = new DateTimeParser[formats.length];
237-
238-
if (formats.length == 1) {
239-
formatter = forPattern(input, locale).parser;
240-
} else {
241-
DateTimeFormatter dateTimeFormatter = null;
242-
for (int i = 0; i < formats.length; i++) {
243-
JodaDateFormatter currentFormatter = forPattern(formats[i], locale);
244-
DateTimeFormatter currentParser = currentFormatter.parser;
245-
if (dateTimeFormatter == null) {
246-
dateTimeFormatter = currentFormatter.printer;
247-
}
248-
parsers[i] = currentParser.getParser();
249-
}
250-
251-
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder()
252-
.append(dateTimeFormatter.withZone(DateTimeZone.UTC).getPrinter(), parsers);
253-
formatter = builder.toFormatter();
254-
}
230+
255231
} else {
256232
try {
257233
formatter = DateTimeFormat.forPattern(input);

server/src/main/java/org/elasticsearch/common/joda/JodaDateFormatter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public JodaDateFormatter(String pattern, DateTimeFormatter parser, DateTimeForma
4747

4848
@Override
4949
public TemporalAccessor parse(String input) {
50-
DateTime dt = parser.parseDateTime(input);
50+
final DateTime dt = parser.parseDateTime(input);
5151
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(dt.getMillis()), DateUtils.dateTimeZoneToZoneId(dt.getZone()));
5252
}
5353

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

Lines changed: 17 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,19 @@
1919

2020
package org.elasticsearch.common.time;
2121

22-
import org.elasticsearch.ElasticsearchParseException;
22+
import org.elasticsearch.common.Strings;
23+
import org.elasticsearch.common.joda.Joda;
2324
import org.joda.time.DateTime;
2425

2526
import java.time.Instant;
2627
import java.time.ZoneId;
28+
import java.time.ZoneOffset;
2729
import java.time.ZonedDateTime;
2830
import java.time.format.DateTimeParseException;
2931
import java.time.temporal.TemporalAccessor;
30-
import java.util.Arrays;
32+
import java.util.ArrayList;
33+
import java.util.List;
3134
import java.util.Locale;
32-
import java.util.stream.Collectors;
3335

3436
public interface DateFormatter {
3537

@@ -85,7 +87,7 @@ default DateTime parseJoda(String input) {
8587
* Return the given millis-since-epoch formatted with this format.
8688
*/
8789
default String formatMillis(long millis) {
88-
return format(Instant.ofEpochMilli(millis));
90+
return format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneOffset.UTC));
8991
}
9092

9193
/**
@@ -123,94 +125,23 @@ default String formatJoda(DateTime dateTime) {
123125
*/
124126
DateMathParser toDateMathParser();
125127

126-
/**
127-
* Merge several date formatters into a single one. Useful if you need to have several formatters with
128-
* different formats act as one, for example when you specify a
129-
* format like <code>date_hour||epoch_millis</code>
130-
*
131-
* @param formatters The list of date formatters to be merged together
132-
* @return The new date formtter containing the specified date formatters
133-
*/
134-
static DateFormatter merge(DateFormatter... formatters) {
135-
return new MergedDateFormatter(formatters);
128+
static DateFormatter forPattern(String input) {
129+
return forPattern(input, Locale.ROOT);
136130
}
137131

138-
class MergedDateFormatter implements DateFormatter {
139-
140-
private final String format;
141-
private final DateFormatter[] formatters;
142-
private final DateMathParser[] dateMathParsers;
143-
144-
MergedDateFormatter(DateFormatter... formatters) {
145-
this.formatters = formatters;
146-
this.format = Arrays.stream(formatters).map(DateFormatter::pattern).collect(Collectors.joining("||"));
147-
this.dateMathParsers = Arrays.stream(formatters).map(DateFormatter::toDateMathParser).toArray(DateMathParser[]::new);
132+
static DateFormatter forPattern(String input, Locale locale) {
133+
if (Strings.hasLength(input) == false) {
134+
throw new IllegalArgumentException("No date pattern provided");
148135
}
149-
150-
@Override
151-
public TemporalAccessor parse(String input) {
152-
DateTimeParseException failure = null;
153-
for (DateFormatter formatter : formatters) {
154-
try {
155-
return formatter.parse(input);
156-
} catch (DateTimeParseException e) {
157-
if (failure == null) {
158-
failure = e;
159-
} else {
160-
failure.addSuppressed(e);
161-
}
162-
}
163-
}
164-
throw failure;
165-
}
166-
167-
@Override
168-
public DateFormatter withZone(ZoneId zoneId) {
169-
return new MergedDateFormatter(Arrays.stream(formatters).map(f -> f.withZone(zoneId)).toArray(DateFormatter[]::new));
136+
List<DateFormatter> formatters = new ArrayList<>();
137+
for (String pattern : Strings.delimitedListToStringArray(input, "||")) {
138+
formatters.add(Joda.forPattern(pattern, locale));
170139
}
171140

172-
@Override
173-
public DateFormatter withLocale(Locale locale) {
174-
return new MergedDateFormatter(Arrays.stream(formatters).map(f -> f.withLocale(locale)).toArray(DateFormatter[]::new));
141+
if (formatters.size() == 1) {
142+
return formatters.get(0);
175143
}
144+
return new DateFormatters.MergedDateFormatter(input, formatters);
176145

177-
@Override
178-
public String format(TemporalAccessor accessor) {
179-
return formatters[0].format(accessor);
180-
}
181-
182-
@Override
183-
public String pattern() {
184-
return format;
185-
}
186-
187-
@Override
188-
public Locale locale() {
189-
return formatters[0].locale();
190-
}
191-
192-
@Override
193-
public ZoneId zone() {
194-
return formatters[0].zone();
195-
}
196-
197-
@Override
198-
public DateMathParser toDateMathParser() {
199-
return (text, now, roundUp, tz) -> {
200-
ElasticsearchParseException failure = null;
201-
for (DateMathParser parser : dateMathParsers) {
202-
try {
203-
return parser.parse(text, now, roundUp, tz);
204-
} catch (ElasticsearchParseException e) {
205-
if (failure == null) {
206-
failure = e;
207-
} else {
208-
failure.addSuppressed(e);
209-
}
210-
}
211-
}
212-
throw failure;
213-
};
214-
}
215146
}
216147
}

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

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,31 @@
1919

2020
package org.elasticsearch.common.time;
2121

22+
import org.elasticsearch.ElasticsearchParseException;
2223
import org.elasticsearch.common.Strings;
2324

2425
import java.time.DateTimeException;
2526
import java.time.DayOfWeek;
2627
import java.time.Instant;
2728
import java.time.LocalDate;
29+
import java.time.ZoneId;
2830
import java.time.ZoneOffset;
2931
import java.time.ZonedDateTime;
3032
import java.time.format.DateTimeFormatter;
3133
import java.time.format.DateTimeFormatterBuilder;
34+
import java.time.format.DateTimeParseException;
3235
import java.time.format.ResolverStyle;
3336
import java.time.format.SignStyle;
3437
import java.time.temporal.ChronoField;
3538
import java.time.temporal.IsoFields;
3639
import java.time.temporal.TemporalAccessor;
3740
import java.time.temporal.TemporalAdjusters;
3841
import java.time.temporal.WeekFields;
42+
import java.util.ArrayList;
43+
import java.util.Collections;
44+
import java.util.List;
3945
import java.util.Locale;
46+
import java.util.stream.Collectors;
4047

4148
import static java.time.temporal.ChronoField.DAY_OF_MONTH;
4249
import static java.time.temporal.ChronoField.DAY_OF_WEEK;
@@ -1442,12 +1449,12 @@ private static DateFormatter forPattern(String input, Locale locale) {
14421449
return forPattern(formats[0], locale);
14431450
} else {
14441451
try {
1445-
DateFormatter[] formatters = new DateFormatter[formats.length];
1452+
List<DateFormatter> formatters = new ArrayList<>(formats.length);
14461453
for (int i = 0; i < formats.length; i++) {
1447-
formatters[i] = forPattern(formats[i], locale);
1454+
formatters.add(forPattern(formats[i], locale));
14481455
}
14491456

1450-
return DateFormatter.merge(formatters);
1457+
return new MergedDateFormatter(input, formatters);
14511458
} catch (IllegalArgumentException e) {
14521459
throw new IllegalArgumentException("Invalid format: [" + input + "]: " + e.getMessage(), e);
14531460
}
@@ -1461,6 +1468,91 @@ private static DateFormatter forPattern(String input, Locale locale) {
14611468
}
14621469
}
14631470

1471+
static class MergedDateFormatter implements DateFormatter {
1472+
1473+
private final String pattern;
1474+
private final List<DateFormatter> formatters;
1475+
private final List<DateMathParser> dateMathParsers;
1476+
1477+
MergedDateFormatter(String pattern, List<DateFormatter> formatters) {
1478+
assert formatters.size() > 0;
1479+
this.pattern = pattern;
1480+
this.formatters = Collections.unmodifiableList(formatters);
1481+
this.dateMathParsers = formatters.stream().map(DateFormatter::toDateMathParser).collect(Collectors.toList());
1482+
}
1483+
1484+
@Override
1485+
public TemporalAccessor parse(String input) {
1486+
IllegalArgumentException failure = null;
1487+
for (DateFormatter formatter : formatters) {
1488+
try {
1489+
return formatter.parse(input);
1490+
// TODO: remove DateTimeParseException when JavaDateFormatter throws IAE
1491+
} catch (IllegalArgumentException | DateTimeParseException e) {
1492+
if (failure == null) {
1493+
// wrap so the entire multi format is in the message
1494+
failure = new IllegalArgumentException("failed to parse date field [" + input + "] with format [" + pattern + "]",
1495+
e);
1496+
} else {
1497+
failure.addSuppressed(e);
1498+
}
1499+
}
1500+
}
1501+
throw failure;
1502+
}
1503+
1504+
@Override
1505+
public DateFormatter withZone(ZoneId zoneId) {
1506+
return new MergedDateFormatter(pattern, formatters.stream().map(f -> f.withZone(zoneId)).collect(Collectors.toList()));
1507+
}
1508+
1509+
@Override
1510+
public DateFormatter withLocale(Locale locale) {
1511+
return new MergedDateFormatter(pattern, formatters.stream().map(f -> f.withLocale(locale)).collect(Collectors.toList()));
1512+
}
1513+
1514+
@Override
1515+
public String format(TemporalAccessor accessor) {
1516+
return formatters.get(0).format(accessor);
1517+
}
1518+
1519+
@Override
1520+
public String pattern() {
1521+
return pattern;
1522+
}
1523+
1524+
@Override
1525+
public Locale locale() {
1526+
return formatters.get(0).locale();
1527+
}
1528+
1529+
@Override
1530+
public ZoneId zone() {
1531+
return formatters.get(0).zone();
1532+
}
1533+
1534+
@Override
1535+
public DateMathParser toDateMathParser() {
1536+
return (text, now, roundUp, tz) -> {
1537+
ElasticsearchParseException failure = null;
1538+
for (DateMathParser parser : dateMathParsers) {
1539+
try {
1540+
return parser.parse(text, now, roundUp, tz);
1541+
} catch (ElasticsearchParseException e) {
1542+
if (failure == null) {
1543+
// wrap so the entire multi format is in the message
1544+
failure = new ElasticsearchParseException("failed to parse date field [" + text + "] with format ["
1545+
+ pattern + "]", e);
1546+
} else {
1547+
failure.addSuppressed(e);
1548+
}
1549+
}
1550+
}
1551+
throw failure;
1552+
};
1553+
}
1554+
}
1555+
14641556
private static final ZonedDateTime EPOCH_ZONED_DATE_TIME = Instant.EPOCH.atZone(ZoneOffset.UTC);
14651557

14661558
public static ZonedDateTime toZonedDateTime(TemporalAccessor accessor) {

server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
import org.elasticsearch.common.Explicit;
3737
import org.elasticsearch.common.Nullable;
3838
import org.elasticsearch.common.geo.ShapeRelation;
39-
import org.elasticsearch.common.joda.Joda;
4039
import org.elasticsearch.common.settings.Settings;
4140
import org.elasticsearch.common.time.DateFormatter;
4241
import org.elasticsearch.common.time.DateMathParser;
@@ -65,7 +64,7 @@
6564
public class DateFieldMapper extends FieldMapper {
6665

6766
public static final String CONTENT_TYPE = "date";
68-
public static final DateFormatter DEFAULT_DATE_TIME_FORMATTER = Joda.forPattern(
67+
public static final DateFormatter DEFAULT_DATE_TIME_FORMATTER = DateFormatter.forPattern(
6968
"strict_date_optional_time||epoch_millis", Locale.ROOT);
7069

7170
public static class Defaults {
@@ -381,7 +380,7 @@ public Object valueForDisplay(Object value) {
381380
public DocValueFormat docValueFormat(@Nullable String format, DateTimeZone timeZone) {
382381
DateFormatter dateTimeFormatter = this.dateTimeFormatter;
383382
if (format != null) {
384-
dateTimeFormatter = Joda.forPattern(format);
383+
dateTimeFormatter = DateFormatter.forPattern(format);
385384
}
386385
if (timeZone == null) {
387386
timeZone = DateTimeZone.UTC;

server/src/main/java/org/elasticsearch/index/mapper/TypeParsers.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
import org.apache.lucene.index.IndexOptions;
2323
import org.elasticsearch.ElasticsearchParseException;
24-
import org.elasticsearch.common.joda.Joda;
2524
import org.elasticsearch.common.time.DateFormatter;
2625
import org.elasticsearch.common.xcontent.support.XContentMapValues;
2726
import org.elasticsearch.index.analysis.NamedAnalyzer;
@@ -265,7 +264,7 @@ private static IndexOptions nodeIndexOptionValue(final Object propNode) {
265264

266265
public static DateFormatter parseDateTimeFormatter(Object node) {
267266
if (node instanceof String) {
268-
return Joda.forPattern((String) node);
267+
return DateFormatter.forPattern((String) node);
269268
}
270269
throw new IllegalArgumentException("Invalid format: [" + node.toString() + "]: expected string value");
271270
}

server/src/main/java/org/elasticsearch/index/query/RangeQueryBuilder.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import org.elasticsearch.common.geo.ShapeRelation;
3030
import org.elasticsearch.common.io.stream.StreamInput;
3131
import org.elasticsearch.common.io.stream.StreamOutput;
32-
import org.elasticsearch.common.joda.Joda;
3332
import org.elasticsearch.common.lucene.BytesRefs;
3433
import org.elasticsearch.common.time.DateFormatter;
3534
import org.elasticsearch.common.time.DateMathParser;
@@ -105,7 +104,7 @@ public RangeQueryBuilder(StreamInput in) throws IOException {
105104
timeZone = in.readOptionalTimeZone();
106105
String formatString = in.readOptionalString();
107106
if (formatString != null) {
108-
format = Joda.forPattern(formatString);
107+
format = DateFormatter.forPattern(formatString);
109108
}
110109
String relationString = in.readOptionalString();
111110
if (relationString != null) {
@@ -290,7 +289,7 @@ public RangeQueryBuilder format(String format) {
290289
if (format == null) {
291290
throw new IllegalArgumentException("format cannot be null");
292291
}
293-
this.format = Joda.forPattern(format);
292+
this.format = DateFormatter.forPattern(format);
294293
return this;
295294
}
296295

0 commit comments

Comments
 (0)