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 ;
2930import java .time .temporal .TemporalAccessor ;
3031import java .time .temporal .TemporalField ;
3132import java .util .Arrays ;
33+ import java .util .Collection ;
34+ import java .util .Collections ;
3235import java .util .HashMap ;
36+ import java .util .List ;
3337import java .util .Locale ;
3438import java .util .Map ;
3539import java .util .Objects ;
@@ -39,6 +43,7 @@ class JavaDateFormatter implements DateFormatter {
3943
4044 // base fields which should be used for default parsing, when we round up for date math
4145 private static final Map <TemporalField , Long > ROUND_UP_BASE_FIELDS = new HashMap <>(6 );
46+
4247 {
4348 ROUND_UP_BASE_FIELDS .put (ChronoField .MONTH_OF_YEAR , 1L );
4449 ROUND_UP_BASE_FIELDS .put (ChronoField .DAY_OF_MONTH , 1L );
@@ -50,22 +55,15 @@ class JavaDateFormatter implements DateFormatter {
5055
5156 private final String format ;
5257 private final DateTimeFormatter printer ;
53- private final DateTimeFormatter parser ;
58+ private final List < DateTimeFormatter > parsers ;
5459 private final DateTimeFormatter roundupParser ;
5560
56- private JavaDateFormatter (String format , DateTimeFormatter printer , DateTimeFormatter roundupParser , DateTimeFormatter parser ) {
57- this .format = format ;
58- this .printer = printer ;
59- this .roundupParser = roundupParser ;
60- this .parser = parser ;
61- }
62-
6361 JavaDateFormatter (String format , DateTimeFormatter printer , DateTimeFormatter ... parsers ) {
6462 this (format , printer , builder -> ROUND_UP_BASE_FIELDS .forEach (builder ::parseDefaulting ), parsers );
6563 }
6664
6765 JavaDateFormatter (String format , DateTimeFormatter printer , Consumer <DateTimeFormatterBuilder > roundupParserConsumer ,
68- DateTimeFormatter ... parsers ) {
66+ DateTimeFormatter ... parsers ) {
6967 if (printer == null ) {
7068 throw new IllegalArgumentException ("printer may not be null" );
7169 }
@@ -79,26 +77,21 @@ private JavaDateFormatter(String format, DateTimeFormatter printer, DateTimeForm
7977 }
8078 this .printer = printer ;
8179 this .format = format ;
80+
8281 if (parsers .length == 0 ) {
83- this .parser = printer ;
84- } else if (parsers .length == 1 ) {
85- this .parser = parsers [0 ];
82+ this .parsers = Collections .singletonList (printer );
8683 } else {
87- DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder ();
88- for (DateTimeFormatter parser : parsers ) {
89- builder .appendOptional (parser );
90- }
91- this .parser = builder .toFormatter (Locale .ROOT );
84+ this .parsers = Arrays .asList (parsers );
9285 }
9386
9487 DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder ();
9588 if (format .contains ("||" ) == false ) {
96- builder .append (this .parser );
89+ builder .append (this .parsers . get ( 0 ) );
9790 }
9891 roundupParserConsumer .accept (builder );
99- DateTimeFormatter roundupFormatter = builder .toFormatter (parser . getLocale ());
92+ DateTimeFormatter roundupFormatter = builder .toFormatter (locale ());
10093 if (printer .getZone () != null ) {
101- roundupFormatter = roundupFormatter .withZone (printer . getZone ());
94+ roundupFormatter = roundupFormatter .withZone (zone ());
10295 }
10396 this .roundupParser = roundupFormatter ;
10497 }
@@ -107,10 +100,6 @@ DateTimeFormatter getRoundupParser() {
107100 return roundupParser ;
108101 }
109102
110- DateTimeFormatter getParser () {
111- return parser ;
112- }
113-
114103 DateTimeFormatter getPrinter () {
115104 return printer ;
116105 }
@@ -122,30 +111,64 @@ public TemporalAccessor parse(String input) {
122111 }
123112
124113 try {
125- return parser . parse (input );
114+ return doParse (input );
126115 } catch (DateTimeParseException e ) {
127116 throw new IllegalArgumentException ("failed to parse date field [" + input + "] with format [" + format + "]" , e );
128117 }
129118 }
130119
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 ();
150+ }
151+
131152 @ Override
132153 public DateFormatter withZone (ZoneId zoneId ) {
133154 // shortcurt to not create new objects unnecessarily
134- if (zoneId .equals (parser . getZone ())) {
155+ if (zoneId .equals (zone ())) {
135156 return this ;
136157 }
137158
138- 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 ]));
139161 }
140162
141163 @ Override
142164 public DateFormatter withLocale (Locale locale ) {
143165 // shortcurt to not create new objects unnecessarily
144- if (locale .equals (parser . getLocale ())) {
166+ if (locale .equals (locale ())) {
145167 return this ;
146168 }
147169
148- 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 ]));
149172 }
150173
151174 @ Override
@@ -170,7 +193,7 @@ public ZoneId zone() {
170193
171194 @ Override
172195 public DateMathParser toDateMathParser () {
173- return new JavaDateMathParser (format , parser , roundupParser );
196+ return new JavaDateMathParser (format , this , getRoundupParser () );
174197 }
175198
176199 @ Override
@@ -186,12 +209,16 @@ public boolean equals(Object obj) {
186209 JavaDateFormatter other = (JavaDateFormatter ) obj ;
187210
188211 return Objects .equals (format , other .format ) &&
189- Objects .equals (locale (), other .locale ()) &&
190- Objects .equals (this .printer .getZone (), other .printer .getZone ());
212+ Objects .equals (locale (), other .locale ()) &&
213+ Objects .equals (this .printer .getZone (), other .printer .getZone ());
191214 }
192215
193216 @ Override
194217 public String toString () {
195218 return String .format (Locale .ROOT , "format[%s] locale[%s]" , format , locale ());
196219 }
220+
221+ Collection <DateTimeFormatter > getParsers () {
222+ return parsers ;
223+ }
197224}
0 commit comments