1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.logging.log4j.core.util.datetime;
18
19 import java.io.IOException;
20 import java.io.ObjectInputStream;
21 import java.io.Serializable;
22 import java.text.DateFormatSymbols;
23 import java.text.ParseException;
24 import java.text.ParsePosition;
25 import java.util.ArrayList;
26 import java.util.Calendar;
27 import java.util.Date;
28 import java.util.HashMap;
29 import java.util.List;
30 import java.util.Locale;
31 import java.util.Map;
32 import java.util.TimeZone;
33 import java.util.concurrent.ConcurrentHashMap;
34 import java.util.concurrent.ConcurrentMap;
35 import java.util.regex.Matcher;
36 import java.util.regex.Pattern;
37
38
39
40
41 public class FastDateParser implements DateParser, Serializable {
42
43
44
45
46 static final Locale JAPANESE_IMPERIAL = new Locale("ja", "JP", "JP");
47
48
49
50
51
52
53 private static final long serialVersionUID = 3L;
54
55 private static final Strategy NUMBER_MONTH_STRATEGY = new NumberStrategy(Calendar.MONTH) {
56 @Override
57 int modify(final int iValue) {
58 return iValue - 1;
59 }
60 };
61
62 private static final Strategy ABBREVIATED_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR) {
63
64
65
66 @Override
67 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
68 int iValue = Integer.parseInt(value);
69 if (iValue < 100) {
70 iValue = parser.adjustYear(iValue);
71 }
72 cal.set(Calendar.YEAR, iValue);
73 }
74 };
75
76 private static final Strategy LITERAL_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR);
77 private static final Strategy WEEK_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_YEAR);
78 private static final Strategy WEEK_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_MONTH);
79 private static final Strategy DAY_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.DAY_OF_YEAR);
80 private static final Strategy DAY_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_MONTH);
81 private static final Strategy DAY_OF_WEEK_IN_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK_IN_MONTH);
82 private static final Strategy DAY_OF_WEEK_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK);
83 private static final Strategy HOUR_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY);
84 private static final Strategy HOUR24_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY) {
85 @Override
86 int modify(final int iValue) {
87 return iValue == 24 ? 0 : iValue;
88 }
89 };
90 private static final Strategy HOUR12_STRATEGY = new NumberStrategy(Calendar.HOUR) {
91 @Override
92 int modify(final int iValue) {
93 return iValue == 12 ? 0 : iValue;
94 }
95 };
96 private static final Strategy HOUR_STRATEGY = new NumberStrategy(Calendar.HOUR);
97 private static final Strategy MINUTE_STRATEGY = new NumberStrategy(Calendar.MINUTE);
98 private static final Strategy SECOND_STRATEGY = new NumberStrategy(Calendar.SECOND);
99 private static final Strategy MILLISECOND_STRATEGY = new NumberStrategy(Calendar.MILLISECOND);
100 private static final Strategy ISO_8601_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}(?::?\\d{2})?))");
101
102
103 private final String pattern;
104 private final TimeZone timeZone;
105 private final Locale locale;
106 private final int century;
107 private final int startYear;
108 private final boolean lenient;
109
110
111 private transient Pattern parsePattern;
112 private transient Strategy[] strategies;
113
114
115 private transient String currentFormatField;
116 private transient Strategy nextStrategy;
117
118
119
120
121
122
123
124
125
126
127
128
129
130 protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale) {
131 this(pattern, timeZone, locale, null, true);
132 }
133
134
135
136
137
138
139
140
141
142
143
144
145
146 protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) {
147 this(pattern, timeZone, locale, centuryStart, true);
148 }
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164 protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale,
165 final Date centuryStart, final boolean lenient) {
166 this.pattern = pattern;
167 this.timeZone = timeZone;
168 this.locale = locale;
169 this.lenient = lenient;
170
171 final Calendar definingCalendar = Calendar.getInstance(timeZone, locale);
172
173 int centuryStartYear;
174 if (centuryStart != null) {
175 definingCalendar.setTime(centuryStart);
176 centuryStartYear = definingCalendar.get(Calendar.YEAR);
177 } else if (locale.equals(JAPANESE_IMPERIAL)) {
178 centuryStartYear = 0;
179 } else {
180
181 definingCalendar.setTime(new Date());
182 centuryStartYear = definingCalendar.get(Calendar.YEAR) - 80;
183 }
184 century = centuryStartYear / 100 * 100;
185 startYear = centuryStartYear - century;
186
187 init(definingCalendar);
188 }
189
190
191
192
193
194
195
196 private void init(final Calendar definingCalendar) {
197
198 final StringBuilder regex = new StringBuilder();
199 final List<Strategy> collector = new ArrayList<>();
200
201 final Matcher patternMatcher = formatPattern.matcher(pattern);
202 if (!patternMatcher.lookingAt()) {
203 throw new IllegalArgumentException("Illegal pattern character '"
204 + pattern.charAt(patternMatcher.regionStart()) + "'");
205 }
206
207 currentFormatField = patternMatcher.group();
208 Strategy currentStrategy = getStrategy(currentFormatField, definingCalendar);
209 for (;;) {
210 patternMatcher.region(patternMatcher.end(), patternMatcher.regionEnd());
211 if (!patternMatcher.lookingAt()) {
212 nextStrategy = null;
213 break;
214 }
215 final String nextFormatField = patternMatcher.group();
216 nextStrategy = getStrategy(nextFormatField, definingCalendar);
217 if (currentStrategy.addRegex(this, regex)) {
218 collector.add(currentStrategy);
219 }
220 currentFormatField = nextFormatField;
221 currentStrategy = nextStrategy;
222 }
223 if (patternMatcher.regionStart() != patternMatcher.regionEnd()) {
224 throw new IllegalArgumentException("Failed to parse \"" + pattern + "\" ; gave up at index "
225 + patternMatcher.regionStart());
226 }
227 if (currentStrategy.addRegex(this, regex)) {
228 collector.add(currentStrategy);
229 }
230 currentFormatField = null;
231 strategies = collector.toArray(new Strategy[collector.size()]);
232 parsePattern = Pattern.compile(regex.toString());
233 }
234
235
236
237
238
239
240
241
242 @Override
243 public String getPattern() {
244 return pattern;
245 }
246
247
248
249
250
251
252 @Override
253 public TimeZone getTimeZone() {
254 return timeZone;
255 }
256
257
258
259
260
261
262 @Override
263 public Locale getLocale() {
264 return locale;
265 }
266
267
268
269
270
271
272 Pattern getParsePattern() {
273 return parsePattern;
274 }
275
276
277
278
279
280
281
282
283
284
285
286 @Override
287 public boolean equals(final Object obj) {
288 if (!(obj instanceof FastDateParser)) {
289 return false;
290 }
291 final FastDateParser other = (FastDateParser) obj;
292 return pattern.equals(other.pattern) && timeZone.equals(other.timeZone) && locale.equals(other.locale);
293 }
294
295
296
297
298
299
300
301
302 @Override
303 public int hashCode() {
304 return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode());
305 }
306
307
308
309
310
311
312
313
314 @Override
315 public String toString() {
316 return "FastDateParser[" + pattern + "," + locale + "," + timeZone.getID() + "]";
317 }
318
319
320
321
322
323
324
325
326
327
328 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
329 in.defaultReadObject();
330
331 final Calendar definingCalendar = Calendar.getInstance(timeZone, locale);
332 init(definingCalendar);
333 }
334
335
336
337
338
339
340 @Override
341 public Object parseObject(final String source) throws ParseException {
342 return parse(source);
343 }
344
345
346
347
348
349
350 @Override
351 public Date parse(final String source) throws ParseException {
352 final Date date = parse(source, new ParsePosition(0));
353 if (date == null) {
354
355 if (locale.equals(JAPANESE_IMPERIAL)) {
356 throw new ParseException("(The " + locale + " locale does not support dates before 1868 AD)\n"
357 + "Unparseable date: \"" + source + "\" does not match " + parsePattern.pattern(), 0);
358 }
359 throw new ParseException("Unparseable date: \"" + source + "\" does not match " + parsePattern.pattern(), 0);
360 }
361 return date;
362 }
363
364
365
366
367
368
369 @Override
370 public Object parseObject(final String source, final ParsePosition pos) {
371 return parse(source, pos);
372 }
373
374
375
376
377
378
379
380
381
382
383
384
385 @Override
386 public Date parse(final String source, final ParsePosition pos) {
387 final int offset = pos.getIndex();
388 final Matcher matcher = parsePattern.matcher(source.substring(offset));
389 if (!matcher.lookingAt()) {
390 return null;
391 }
392
393 final Calendar cal = Calendar.getInstance(timeZone, locale);
394 cal.clear();
395 cal.setLenient(lenient);
396
397 for (int i = 0; i < strategies.length;) {
398 final Strategy strategy = strategies[i++];
399 strategy.setCalendar(this, cal, matcher.group(i));
400 }
401 pos.setIndex(offset + matcher.end());
402 return cal.getTime();
403 }
404
405
406
407
408 private static StringBuilder simpleQuote(final StringBuilder sb, final String value) {
409 for (int i = 0; i < value.length(); ++i) {
410 final char c = value.charAt(i);
411 switch (c) {
412 case '\\':
413 case '^':
414 case '$':
415 case '.':
416 case '|':
417 case '?':
418 case '*':
419 case '+':
420 case '(':
421 case ')':
422 case '[':
423 case '{':
424 sb.append('\\');
425 default:
426 sb.append(c);
427 }
428 }
429 return sb;
430 }
431
432
433
434
435
436
437
438
439
440 private static StringBuilder escapeRegex(final StringBuilder regex, final String value, final boolean unquote) {
441 regex.append("\\Q");
442 for (int i = 0; i < value.length(); ++i) {
443 char c = value.charAt(i);
444 switch (c) {
445 case '\'':
446 if (unquote) {
447 if (++i == value.length()) {
448 return regex;
449 }
450 c = value.charAt(i);
451 }
452 break;
453 case '\\':
454 if (++i == value.length()) {
455 break;
456 }
457
458
459
460
461
462
463
464 regex.append(c);
465 c = value.charAt(i);
466 if (c == 'E') {
467 regex.append("E\\\\E\\");
468 c = 'Q';
469 }
470 break;
471 default:
472 break;
473 }
474 regex.append(c);
475 }
476 regex.append("\\E");
477 return regex;
478 }
479
480
481
482
483
484
485
486
487
488 private static Map<String, Integer> getDisplayNames(final int field, final Calendar definingCalendar,
489 final Locale locale) {
490 return definingCalendar.getDisplayNames(field, Calendar.ALL_STYLES, locale);
491 }
492
493
494
495
496
497
498
499 private int adjustYear(final int twoDigitYear) {
500 final int trial = century + twoDigitYear;
501 return twoDigitYear >= startYear ? trial : trial + 100;
502 }
503
504
505
506
507
508
509 boolean isNextNumber() {
510 return nextStrategy != null && nextStrategy.isNumber();
511 }
512
513
514
515
516
517
518 int getFieldWidth() {
519 return currentFormatField.length();
520 }
521
522
523
524
525 private static abstract class Strategy {
526
527
528
529
530
531
532 boolean isNumber() {
533 return false;
534 }
535
536
537
538
539
540
541
542
543
544
545 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
546
547 }
548
549
550
551
552
553
554
555
556
557 abstract boolean addRegex(FastDateParser parser, StringBuilder regex);
558
559 }
560
561
562
563
564 private static final Pattern formatPattern = Pattern
565 .compile("D+|E+|F+|G+|H+|K+|M+|S+|W+|X+|Z+|a+|d+|h+|k+|m+|s+|u+|w+|y+|z+|''|'[^']++(''[^']*+)*+'|[^'A-Za-z]++");
566
567
568
569
570
571
572
573
574 private Strategy getStrategy(final String formatField, final Calendar definingCalendar) {
575 switch (formatField.charAt(0)) {
576 case '\'':
577 if (formatField.length() > 2) {
578 return new CopyQuotedStrategy(formatField.substring(1, formatField.length() - 1));
579 }
580
581 default:
582 return new CopyQuotedStrategy(formatField);
583 case 'D':
584 return DAY_OF_YEAR_STRATEGY;
585 case 'E':
586 return getLocaleSpecificStrategy(Calendar.DAY_OF_WEEK, definingCalendar);
587 case 'F':
588 return DAY_OF_WEEK_IN_MONTH_STRATEGY;
589 case 'G':
590 return getLocaleSpecificStrategy(Calendar.ERA, definingCalendar);
591 case 'H':
592 return HOUR_OF_DAY_STRATEGY;
593 case 'K':
594 return HOUR_STRATEGY;
595 case 'M':
596 return formatField.length() >= 3 ? getLocaleSpecificStrategy(Calendar.MONTH, definingCalendar)
597 : NUMBER_MONTH_STRATEGY;
598 case 'S':
599 return MILLISECOND_STRATEGY;
600 case 'W':
601 return WEEK_OF_MONTH_STRATEGY;
602 case 'a':
603 return getLocaleSpecificStrategy(Calendar.AM_PM, definingCalendar);
604 case 'd':
605 return DAY_OF_MONTH_STRATEGY;
606 case 'h':
607 return HOUR12_STRATEGY;
608 case 'k':
609 return HOUR24_OF_DAY_STRATEGY;
610 case 'm':
611 return MINUTE_STRATEGY;
612 case 's':
613 return SECOND_STRATEGY;
614 case 'u':
615 return DAY_OF_WEEK_STRATEGY;
616 case 'w':
617 return WEEK_OF_YEAR_STRATEGY;
618 case 'y':
619 return formatField.length() > 2 ? LITERAL_YEAR_STRATEGY : ABBREVIATED_YEAR_STRATEGY;
620 case 'X':
621 return ISO8601TimeZoneStrategy.getStrategy(formatField.length());
622 case 'Z':
623 if (formatField.equals("ZZ")) {
624 return ISO_8601_STRATEGY;
625 }
626
627 case 'z':
628 return getLocaleSpecificStrategy(Calendar.ZONE_OFFSET, definingCalendar);
629 }
630 }
631
632 @SuppressWarnings("unchecked")
633
634 private static final ConcurrentMap<Locale, Strategy>[] caches = new ConcurrentMap[Calendar.FIELD_COUNT];
635
636
637
638
639
640
641
642 private static ConcurrentMap<Locale, Strategy> getCache(final int field) {
643 synchronized (caches) {
644 if (caches[field] == null) {
645 caches[field] = new ConcurrentHashMap<>(3);
646 }
647 return caches[field];
648 }
649 }
650
651
652
653
654
655
656
657
658 private Strategy getLocaleSpecificStrategy(final int field, final Calendar definingCalendar) {
659 final ConcurrentMap<Locale, Strategy> cache = getCache(field);
660 Strategy strategy = cache.get(locale);
661 if (strategy == null) {
662 strategy = field == Calendar.ZONE_OFFSET ? new TimeZoneStrategy(locale) : new CaseInsensitiveTextStrategy(
663 field, definingCalendar, locale);
664 final Strategy inCache = cache.putIfAbsent(locale, strategy);
665 if (inCache != null) {
666 return inCache;
667 }
668 }
669 return strategy;
670 }
671
672
673
674
675 private static class CopyQuotedStrategy extends Strategy {
676 private final String formatField;
677
678
679
680
681
682
683 CopyQuotedStrategy(final String formatField) {
684 this.formatField = formatField;
685 }
686
687
688
689
690 @Override
691 boolean isNumber() {
692 char c = formatField.charAt(0);
693 if (c == '\'') {
694 c = formatField.charAt(1);
695 }
696 return Character.isDigit(c);
697 }
698
699
700
701
702 @Override
703 boolean addRegex(final FastDateParser parser, final StringBuilder regex) {
704 escapeRegex(regex, formatField, true);
705 return false;
706 }
707 }
708
709
710
711
712 private static class CaseInsensitiveTextStrategy extends Strategy {
713 private final int field;
714 private final Locale locale;
715 private final Map<String, Integer> lKeyValues;
716
717
718
719
720
721
722
723
724 CaseInsensitiveTextStrategy(final int field, final Calendar definingCalendar, final Locale locale) {
725 this.field = field;
726 this.locale = locale;
727 final Map<String, Integer> keyValues = getDisplayNames(field, definingCalendar, locale);
728 this.lKeyValues = new HashMap<>();
729
730 for (final Map.Entry<String, Integer> entry : keyValues.entrySet()) {
731 lKeyValues.put(entry.getKey().toLowerCase(locale), entry.getValue());
732 }
733 }
734
735
736
737
738 @Override
739 boolean addRegex(final FastDateParser parser, final StringBuilder regex) {
740 regex.append("((?iu)");
741 for (final String textKeyValue : lKeyValues.keySet()) {
742 simpleQuote(regex, textKeyValue).append('|');
743 }
744 regex.setCharAt(regex.length() - 1, ')');
745 return true;
746 }
747
748
749
750
751 @Override
752 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
753 final Integer iVal = lKeyValues.get(value.toLowerCase(locale));
754 if (iVal == null) {
755 final StringBuilder sb = new StringBuilder(value);
756 sb.append(" not in (");
757 for (final String textKeyValue : lKeyValues.keySet()) {
758 sb.append(textKeyValue).append(' ');
759 }
760 sb.setCharAt(sb.length() - 1, ')');
761 throw new IllegalArgumentException(sb.toString());
762 }
763 cal.set(field, iVal.intValue());
764 }
765 }
766
767
768
769
770 private static class NumberStrategy extends Strategy {
771 private final int field;
772
773
774
775
776
777
778 NumberStrategy(final int field) {
779 this.field = field;
780 }
781
782
783
784
785 @Override
786 boolean isNumber() {
787 return true;
788 }
789
790
791
792
793 @Override
794 boolean addRegex(final FastDateParser parser, final StringBuilder regex) {
795
796 if (parser.isNextNumber()) {
797 regex.append("(\\p{Nd}{").append(parser.getFieldWidth()).append("}+)");
798 } else {
799 regex.append("(\\p{Nd}++)");
800 }
801 return true;
802 }
803
804
805
806
807 @Override
808 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
809 cal.set(field, modify(Integer.parseInt(value)));
810 }
811
812
813
814
815
816
817
818 int modify(final int iValue) {
819 return iValue;
820 }
821 }
822
823
824
825
826 static class TimeZoneStrategy extends Strategy {
827 private static final String RFC_822_TIME_ZONE = "[+-]\\d{4}";
828 private static final String GMT_OPTION = "GMT[+-]\\d{1,2}:\\d{2}";
829
830 private final Locale locale;
831 private final Map<String, TimeZone> tzNames = new HashMap<>();
832 private final String validTimeZoneChars;
833
834
835
836
837 private static final int ID = 0;
838
839
840
841
842
843
844 TimeZoneStrategy(final Locale locale) {
845 this.locale = locale;
846
847 final StringBuilder sb = new StringBuilder();
848 sb.append('(' + RFC_822_TIME_ZONE + "|(?iu)" + GMT_OPTION);
849
850 final String[][] zones = DateFormatSymbols.getInstance(locale).getZoneStrings();
851 for (final String[] zoneNames : zones) {
852 final String tzId = zoneNames[ID];
853 if (tzId.equalsIgnoreCase("GMT")) {
854 continue;
855 }
856 final TimeZone tz = TimeZone.getTimeZone(tzId);
857 for (int i = 1; i < zoneNames.length; ++i) {
858 final String zoneName = zoneNames[i].toLowerCase(locale);
859 if (!tzNames.containsKey(zoneName)) {
860 tzNames.put(zoneName, tz);
861 simpleQuote(sb.append('|'), zoneName);
862 }
863 }
864 }
865
866 sb.append(')');
867 validTimeZoneChars = sb.toString();
868 }
869
870
871
872
873 @Override
874 boolean addRegex(final FastDateParser parser, final StringBuilder regex) {
875 regex.append(validTimeZoneChars);
876 return true;
877 }
878
879
880
881
882 @Override
883 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
884 TimeZone tz;
885 if (value.charAt(0) == '+' || value.charAt(0) == '-') {
886 tz = TimeZone.getTimeZone("GMT" + value);
887 } else if (value.regionMatches(true, 0, "GMT", 0, 3)) {
888 tz = TimeZone.getTimeZone(value.toUpperCase());
889 } else {
890 tz = tzNames.get(value.toLowerCase(locale));
891 if (tz == null) {
892 throw new IllegalArgumentException(value + " is not a supported timezone name");
893 }
894 }
895 cal.setTimeZone(tz);
896 }
897 }
898
899 private static class ISO8601TimeZoneStrategy extends Strategy {
900
901 private final String pattern;
902
903
904
905
906
907
908 ISO8601TimeZoneStrategy(final String pattern) {
909 this.pattern = pattern;
910 }
911
912
913
914
915 @Override
916 boolean addRegex(final FastDateParser parser, final StringBuilder regex) {
917 regex.append(pattern);
918 return true;
919 }
920
921
922
923
924 @Override
925 void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
926 if (value.equals("Z")) {
927 cal.setTimeZone(TimeZone.getTimeZone("UTC"));
928 } else {
929 cal.setTimeZone(TimeZone.getTimeZone("GMT" + value));
930 }
931 }
932
933 private static final Strategy ISO_8601_1_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}))");
934 private static final Strategy ISO_8601_2_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}\\d{2}))");
935 private static final Strategy ISO_8601_3_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}(?::)\\d{2}))");
936
937
938
939
940
941
942
943
944 static Strategy getStrategy(final int tokenLen) {
945 switch (tokenLen) {
946 case 1:
947 return ISO_8601_1_STRATEGY;
948 case 2:
949 return ISO_8601_2_STRATEGY;
950 case 3:
951 return ISO_8601_3_STRATEGY;
952 default:
953 throw new IllegalArgumentException("invalid number of X");
954 }
955 }
956 }
957
958 }