2121
2222import org .elasticsearch .common .Strings ;
2323
24+ import java .text .ParsePosition ;
2425import java .time .ZoneId ;
2526import java .time .format .DateTimeFormatter ;
2627import java .time .format .DateTimeFormatterBuilder ;
28+ import java .time .format .DateTimeParseException ;
2729import java .time .temporal .ChronoField ;
2830import java .time .temporal .TemporalAccessor ;
2931import java .time .temporal .TemporalField ;
3032import java .util .Arrays ;
33+ import java .util .Collection ;
34+ import java .util .Collections ;
3135import java .util .HashMap ;
36+ import java .util .List ;
3237import java .util .Locale ;
3338import java .util .Map ;
3439import java .util .Objects ;
@@ -38,6 +43,7 @@ class JavaDateFormatter implements DateFormatter {
3843
3944 // base fields which should be used for default parsing, when we round up for date math
4045 private static final Map <TemporalField , Long > ROUND_UP_BASE_FIELDS = new HashMap <>(6 );
46+
4147 {
4248 ROUND_UP_BASE_FIELDS .put (ChronoField .MONTH_OF_YEAR , 1L );
4349 ROUND_UP_BASE_FIELDS .put (ChronoField .DAY_OF_MONTH , 1L );
@@ -49,22 +55,15 @@ class JavaDateFormatter implements DateFormatter {
4955
5056 private final String format ;
5157 private final DateTimeFormatter printer ;
52- private final DateTimeFormatter parser ;
58+ private final List < DateTimeFormatter > parsers ;
5359 private final DateTimeFormatter roundupParser ;
5460
55- private JavaDateFormatter (String format , DateTimeFormatter printer , DateTimeFormatter roundupParser , DateTimeFormatter parser ) {
56- this .format = "8" + format ;
57- this .printer = printer ;
58- this .roundupParser = roundupParser ;
59- this .parser = parser ;
60- }
61-
6261 JavaDateFormatter (String format , DateTimeFormatter printer , DateTimeFormatter ... parsers ) {
6362 this (format , printer , builder -> ROUND_UP_BASE_FIELDS .forEach (builder ::parseDefaulting ), parsers );
6463 }
6564
6665 JavaDateFormatter (String format , DateTimeFormatter printer , Consumer <DateTimeFormatterBuilder > roundupParserConsumer ,
67- DateTimeFormatter ... parsers ) {
66+ DateTimeFormatter ... parsers ) {
6867 if (printer == null ) {
6968 throw new IllegalArgumentException ("printer may not be null" );
7069 }
@@ -76,28 +75,23 @@ private JavaDateFormatter(String format, DateTimeFormatter printer, DateTimeForm
7675 if (distinctLocales > 1 ) {
7776 throw new IllegalArgumentException ("formatters must have the same locale" );
7877 }
78+ this .printer = printer ;
79+ this .format = "8" + format ;
80+
7981 if (parsers .length == 0 ) {
80- this .parser = printer ;
81- } else if (parsers .length == 1 ) {
82- this .parser = parsers [0 ];
82+ this .parsers = Collections .singletonList (printer );
8383 } else {
84- DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder ();
85- for (DateTimeFormatter parser : parsers ) {
86- builder .appendOptional (parser );
87- }
88- this .parser = builder .toFormatter (Locale .ROOT );
84+ this .parsers = Arrays .asList (parsers );
8985 }
90- this .format = "8" + format ;
91- this .printer = printer ;
9286
9387 DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder ();
9488 if (format .contains ("||" ) == false ) {
95- builder .append (this .parser );
89+ builder .append (this .parsers . get ( 0 ) );
9690 }
9791 roundupParserConsumer .accept (builder );
98- DateTimeFormatter roundupFormatter = builder .toFormatter (parser . getLocale ());
92+ DateTimeFormatter roundupFormatter = builder .toFormatter (locale ());
9993 if (printer .getZone () != null ) {
100- roundupFormatter = roundupFormatter .withZone (printer . getZone ());
94+ roundupFormatter = roundupFormatter .withZone (zone ());
10195 }
10296 this .roundupParser = roundupFormatter ;
10397 }
@@ -106,10 +100,6 @@ DateTimeFormatter getRoundupParser() {
106100 return roundupParser ;
107101 }
108102
109- DateTimeFormatter getParser () {
110- return parser ;
111- }
112-
113103 DateTimeFormatter getPrinter () {
114104 return printer ;
115105 }
@@ -119,27 +109,66 @@ public TemporalAccessor parse(String input) {
119109 if (Strings .isNullOrEmpty (input )) {
120110 throw new IllegalArgumentException ("cannot parse empty date" );
121111 }
122- return parser .parse (input );
112+
113+ try {
114+ return doParse (input );
115+ } catch (DateTimeParseException e ) {
116+ throw new IllegalArgumentException ("failed to parse date field [" + input + "] with format [" + format + "]" , e );
117+ }
118+ }
119+
120+ /**
121+ * Attempt parsing the input without throwing exception. If multiple parsers are provided,
122+ * it will continue iterating if the previous parser failed. The pattern must fully match, meaning whole input was used.
123+ * This also means that this method depends on <code>DateTimeFormatter.ClassicFormat.parseObject</code>
124+ * which does not throw exceptions when parsing failed.
125+ *
126+ * The approach with collection of parsers was taken because java-time requires ordering on optional (composite)
127+ * patterns. Joda does not suffer from this.
128+ * https://bugs.openjdk.java.net/browse/JDK-8188771
129+ *
130+ * @param input An arbitrary string resembling the string representation of a date or time
131+ * @return a TemporalAccessor if parsing was successful.
132+ * @throws DateTimeParseException when unable to parse with any parsers
133+ */
134+ private TemporalAccessor doParse (String input ) {
135+ if (parsers .size () > 1 ) {
136+ for (DateTimeFormatter formatter : parsers ) {
137+ ParsePosition pos = new ParsePosition (0 );
138+ Object object = formatter .toFormat ().parseObject (input , pos );
139+ if (parsingSucceeded (object , input , pos ) == true ) {
140+ return (TemporalAccessor ) object ;
141+ }
142+ }
143+ throw new DateTimeParseException ("Failed to parse with all enclosed parsers" , input , 0 );
144+ }
145+ return this .parsers .get (0 ).parse (input );
146+ }
147+
148+ private boolean parsingSucceeded (Object object , String input , ParsePosition pos ) {
149+ return object != null && pos .getIndex () == input .length ();
123150 }
124151
125152 @ Override
126153 public DateFormatter withZone (ZoneId zoneId ) {
127154 // shortcurt to not create new objects unnecessarily
128- if (zoneId .equals (parser . getZone ())) {
155+ if (zoneId .equals (zone ())) {
129156 return this ;
130157 }
131158
132- return new JavaDateFormatter (format , printer .withZone (zoneId ), roundupParser .withZone (zoneId ), parser .withZone (zoneId ));
159+ return new JavaDateFormatter (format , printer .withZone (zoneId ),
160+ parsers .stream ().map (p -> p .withZone (zoneId )).toArray (size -> new DateTimeFormatter [size ]));
133161 }
134162
135163 @ Override
136164 public DateFormatter withLocale (Locale locale ) {
137165 // shortcurt to not create new objects unnecessarily
138- if (locale .equals (parser . getLocale ())) {
166+ if (locale .equals (locale ())) {
139167 return this ;
140168 }
141169
142- return new JavaDateFormatter (format , printer .withLocale (locale ), roundupParser .withLocale (locale ), parser .withLocale (locale ));
170+ return new JavaDateFormatter (format , printer .withLocale (locale ),
171+ parsers .stream ().map (p -> p .withLocale (locale )).toArray (size -> new DateTimeFormatter [size ]));
143172 }
144173
145174 @ Override
@@ -164,7 +193,7 @@ public ZoneId zone() {
164193
165194 @ Override
166195 public DateMathParser toDateMathParser () {
167- return new JavaDateMathParser (format , parser , roundupParser );
196+ return new JavaDateMathParser (format , this , getRoundupParser () );
168197 }
169198
170199 @ Override
@@ -180,12 +209,16 @@ public boolean equals(Object obj) {
180209 JavaDateFormatter other = (JavaDateFormatter ) obj ;
181210
182211 return Objects .equals (format , other .format ) &&
183- Objects .equals (locale (), other .locale ()) &&
184- Objects .equals (this .printer .getZone (), other .printer .getZone ());
212+ Objects .equals (locale (), other .locale ()) &&
213+ Objects .equals (this .printer .getZone (), other .printer .getZone ());
185214 }
186215
187216 @ Override
188217 public String toString () {
189218 return String .format (Locale .ROOT , "format[%s] locale[%s]" , format , locale ());
190219 }
220+
221+ Collection <DateTimeFormatter > getParsers () {
222+ return parsers ;
223+ }
191224}
0 commit comments