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