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