001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.lang3.time; 018 019import java.io.IOException; 020import java.io.ObjectInputStream; 021import java.io.Serializable; 022import java.text.DateFormatSymbols; 023import java.text.ParseException; 024import java.text.ParsePosition; 025import java.text.SimpleDateFormat; 026import java.util.ArrayList; 027import java.util.Calendar; 028import java.util.Comparator; 029import java.util.Date; 030import java.util.HashMap; 031import java.util.List; 032import java.util.ListIterator; 033import java.util.Locale; 034import java.util.Map; 035import java.util.Objects; 036import java.util.Set; 037import java.util.TimeZone; 038import java.util.TreeSet; 039import java.util.concurrent.ConcurrentHashMap; 040import java.util.concurrent.ConcurrentMap; 041import java.util.regex.Matcher; 042import java.util.regex.Pattern; 043 044import org.apache.commons.lang3.LocaleUtils; 045 046/** 047 * FastDateParser is a fast and thread-safe version of {@link java.text.SimpleDateFormat}. 048 * 049 * <p> 050 * To obtain a proxy to a FastDateParser, use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another variation of the factory methods of 051 * {@link FastDateFormat}. 052 * </p> 053 * 054 * <p> 055 * Since FastDateParser is thread safe, you can use a static member instance: 056 * </p> 057 * {@code 058 * private static final DateParser DATE_PARSER = FastDateFormat.getInstance("yyyy-MM-dd"); 059 * } 060 * 061 * <p> 062 * This class can be used as a direct replacement for {@link SimpleDateFormat} in most parsing situations. This class is especially useful in multi-threaded 063 * server environments. {@link SimpleDateFormat} is not thread-safe in any JDK version, nor will it be as Sun has closed the 064 * <a href="https://bugs.openjdk.org/browse/JDK-4228335">bug</a>/RFE. 065 * </p> 066 * 067 * <p> 068 * Only parsing is supported by this class, but all patterns are compatible with SimpleDateFormat. 069 * </p> 070 * 071 * <p> 072 * The class operates in lenient mode, so for example a time of 90 minutes is treated as 1 hour 30 minutes. 073 * </p> 074 * 075 * <p> 076 * Timing tests indicate this class is as about as fast as SimpleDateFormat in single thread applications and about 25% faster in multi-thread applications. 077 * </p> 078 * 079 * @since 3.2 080 * @see FastDatePrinter 081 */ 082public class FastDateParser implements DateParser, Serializable { 083 084 /** 085 * A strategy that handles a text field in the parsing pattern 086 */ 087 private static final class CaseInsensitiveTextStrategy extends PatternStrategy { 088 private final int field; 089 final Locale locale; 090 private final Map<String, Integer> lKeyValues; 091 092 /** 093 * Constructs a Strategy that parses a Text field 094 * 095 * @param field The Calendar field 096 * @param definingCalendar The Calendar to use 097 * @param locale The Locale to use 098 */ 099 CaseInsensitiveTextStrategy(final int field, final Calendar definingCalendar, final Locale locale) { 100 this.field = field; 101 this.locale = LocaleUtils.toLocale(locale); 102 103 final StringBuilder regex = new StringBuilder(); 104 regex.append("((?iu)"); 105 lKeyValues = appendDisplayNames(definingCalendar, locale, field, regex); 106 regex.setLength(regex.length() - 1); 107 regex.append(")"); 108 createPattern(regex); 109 } 110 111 /** 112 * {@inheritDoc} 113 */ 114 @Override 115 void setCalendar(final FastDateParser parser, final Calendar calendar, final String value) { 116 final String lowerCase = value.toLowerCase(locale); 117 Integer iVal = lKeyValues.get(lowerCase); 118 if (iVal == null) { 119 // match missing the optional trailing period 120 iVal = lKeyValues.get(lowerCase + '.'); 121 } 122 // LANG-1669: Mimic fix done in OpenJDK 17 to resolve issue with parsing newly supported day periods added in OpenJDK 16 123 if (Calendar.AM_PM != this.field || iVal <= 1) { 124 calendar.set(field, iVal.intValue()); 125 } 126 } 127 128 /** 129 * Converts this instance to a handy debug string. 130 * 131 * @since 3.12.0 132 */ 133 @Override 134 public String toString() { 135 return "CaseInsensitiveTextStrategy [field=" + field + ", locale=" + locale + ", lKeyValues=" + lKeyValues + ", pattern=" + pattern + "]"; 136 } 137 } 138 139 /** 140 * A strategy that copies the static or quoted field in the parsing pattern 141 */ 142 private static final class CopyQuotedStrategy extends Strategy { 143 144 private final String formatField; 145 146 /** 147 * Constructs a Strategy that ensures the formatField has literal text 148 * 149 * @param formatField The literal text to match 150 */ 151 CopyQuotedStrategy(final String formatField) { 152 this.formatField = formatField; 153 } 154 155 /** 156 * {@inheritDoc} 157 */ 158 @Override 159 boolean isNumber() { 160 return false; 161 } 162 163 @Override 164 boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) { 165 for (int idx = 0; idx < formatField.length(); ++idx) { 166 final int sIdx = idx + pos.getIndex(); 167 if (sIdx == source.length()) { 168 pos.setErrorIndex(sIdx); 169 return false; 170 } 171 if (formatField.charAt(idx) != source.charAt(sIdx)) { 172 pos.setErrorIndex(sIdx); 173 return false; 174 } 175 } 176 pos.setIndex(formatField.length() + pos.getIndex()); 177 return true; 178 } 179 180 /** 181 * Converts this instance to a handy debug string. 182 * 183 * @since 3.12.0 184 */ 185 @Override 186 public String toString() { 187 return "CopyQuotedStrategy [formatField=" + formatField + "]"; 188 } 189 } 190 191 private static final class ISO8601TimeZoneStrategy extends PatternStrategy { 192 // Z, +hh, -hh, +hhmm, -hhmm, +hh:mm or -hh:mm 193 194 private static final Strategy ISO_8601_1_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}))"); 195 196 private static final Strategy ISO_8601_2_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}\\d{2}))"); 197 198 private static final Strategy ISO_8601_3_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}(?::)\\d{2}))"); 199 /** 200 * Factory method for ISO8601TimeZoneStrategies. 201 * 202 * @param tokenLen a token indicating the length of the TimeZone String to be formatted. 203 * @return a ISO8601TimeZoneStrategy that can format TimeZone String of length {@code tokenLen}. If no such strategy exists, an IllegalArgumentException 204 * will be thrown. 205 */ 206 static Strategy getStrategy(final int tokenLen) { 207 switch (tokenLen) { 208 case 1: 209 return ISO_8601_1_STRATEGY; 210 case 2: 211 return ISO_8601_2_STRATEGY; 212 case 3: 213 return ISO_8601_3_STRATEGY; 214 default: 215 throw new IllegalArgumentException("invalid number of X"); 216 } 217 } 218 /** 219 * Constructs a Strategy that parses a TimeZone 220 * 221 * @param pattern The Pattern 222 */ 223 ISO8601TimeZoneStrategy(final String pattern) { 224 createPattern(pattern); 225 } 226 227 /** 228 * {@inheritDoc} 229 */ 230 @Override 231 void setCalendar(final FastDateParser parser, final Calendar calendar, final String value) { 232 calendar.setTimeZone(FastTimeZone.getGmtTimeZone(value)); 233 } 234 } 235 236 /** 237 * A strategy that handles a number field in the parsing pattern 238 */ 239 private static class NumberStrategy extends Strategy { 240 241 private final int field; 242 243 /** 244 * Constructs a Strategy that parses a Number field 245 * 246 * @param field The Calendar field 247 */ 248 NumberStrategy(final int field) { 249 this.field = field; 250 } 251 252 /** 253 * {@inheritDoc} 254 */ 255 @Override 256 boolean isNumber() { 257 return true; 258 } 259 260 /** 261 * Make any modifications to parsed integer 262 * 263 * @param parser The parser 264 * @param iValue The parsed integer 265 * @return The modified value 266 */ 267 int modify(final FastDateParser parser, final int iValue) { 268 return iValue; 269 } 270 271 @Override 272 boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) { 273 int idx = pos.getIndex(); 274 int last = source.length(); 275 276 if (maxWidth == 0) { 277 // if no maxWidth, strip leading white space 278 for (; idx < last; ++idx) { 279 final char c = source.charAt(idx); 280 if (!Character.isWhitespace(c)) { 281 break; 282 } 283 } 284 pos.setIndex(idx); 285 } else { 286 final int end = idx + maxWidth; 287 if (last > end) { 288 last = end; 289 } 290 } 291 292 for (; idx < last; ++idx) { 293 final char c = source.charAt(idx); 294 if (!Character.isDigit(c)) { 295 break; 296 } 297 } 298 299 if (pos.getIndex() == idx) { 300 pos.setErrorIndex(idx); 301 return false; 302 } 303 304 final int value = Integer.parseInt(source.substring(pos.getIndex(), idx)); 305 pos.setIndex(idx); 306 307 calendar.set(field, modify(parser, value)); 308 return true; 309 } 310 311 /** 312 * Converts this instance to a handy debug string. 313 * 314 * @since 3.12.0 315 */ 316 @Override 317 public String toString() { 318 return "NumberStrategy [field=" + field + "]"; 319 } 320 } 321 322 /** 323 * A strategy to parse a single field from the parsing pattern 324 */ 325 private abstract static class PatternStrategy extends Strategy { 326 327 Pattern pattern; 328 329 void createPattern(final String regex) { 330 this.pattern = Pattern.compile(regex); 331 } 332 333 void createPattern(final StringBuilder regex) { 334 createPattern(regex.toString()); 335 } 336 337 /** 338 * Is this field a number? The default implementation returns false. 339 * 340 * @return true, if field is a number 341 */ 342 @Override 343 boolean isNumber() { 344 return false; 345 } 346 347 @Override 348 boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) { 349 final Matcher matcher = pattern.matcher(source.substring(pos.getIndex())); 350 if (!matcher.lookingAt()) { 351 pos.setErrorIndex(pos.getIndex()); 352 return false; 353 } 354 pos.setIndex(pos.getIndex() + matcher.end(1)); 355 setCalendar(parser, calendar, matcher.group(1)); 356 return true; 357 } 358 359 abstract void setCalendar(FastDateParser parser, Calendar calendar, String value); 360 361 /** 362 * Converts this instance to a handy debug string. 363 * 364 * @since 3.12.0 365 */ 366 @Override 367 public String toString() { 368 return getClass().getSimpleName() + " [pattern=" + pattern + "]"; 369 } 370 371 } 372 373 /** 374 * A strategy to parse a single field from the parsing pattern 375 */ 376 private abstract static class Strategy { 377 378 /** 379 * Is this field a number? The default implementation returns false. 380 * 381 * @return true, if field is a number 382 */ 383 boolean isNumber() { 384 return false; 385 } 386 387 abstract boolean parse(FastDateParser parser, Calendar calendar, String source, ParsePosition pos, int maxWidth); 388 } 389 390 /** 391 * Holds strategy and field width 392 */ 393 private static final class StrategyAndWidth { 394 395 final Strategy strategy; 396 final int width; 397 398 StrategyAndWidth(final Strategy strategy, final int width) { 399 this.strategy = Objects.requireNonNull(strategy, "strategy"); 400 this.width = width; 401 } 402 403 int getMaxWidth(final ListIterator<StrategyAndWidth> lt) { 404 if (!strategy.isNumber() || !lt.hasNext()) { 405 return 0; 406 } 407 final Strategy nextStrategy = lt.next().strategy; 408 lt.previous(); 409 return nextStrategy.isNumber() ? width : 0; 410 } 411 412 @Override 413 public String toString() { 414 return "StrategyAndWidth [strategy=" + strategy + ", width=" + width + "]"; 415 } 416 } 417 418 /** 419 * Parse format into Strategies 420 */ 421 private final class StrategyParser { 422 private final Calendar definingCalendar; 423 private int currentIdx; 424 425 StrategyParser(final Calendar definingCalendar) { 426 this.definingCalendar = Objects.requireNonNull(definingCalendar, "definingCalendar"); 427 } 428 429 StrategyAndWidth getNextStrategy() { 430 if (currentIdx >= pattern.length()) { 431 return null; 432 } 433 434 final char c = pattern.charAt(currentIdx); 435 if (isFormatLetter(c)) { 436 return letterPattern(c); 437 } 438 return literal(); 439 } 440 441 private StrategyAndWidth letterPattern(final char c) { 442 final int begin = currentIdx; 443 while (++currentIdx < pattern.length()) { 444 if (pattern.charAt(currentIdx) != c) { 445 break; 446 } 447 } 448 449 final int width = currentIdx - begin; 450 return new StrategyAndWidth(getStrategy(c, width, definingCalendar), width); 451 } 452 453 private StrategyAndWidth literal() { 454 boolean activeQuote = false; 455 456 final StringBuilder sb = new StringBuilder(); 457 while (currentIdx < pattern.length()) { 458 final char c = pattern.charAt(currentIdx); 459 if (!activeQuote && isFormatLetter(c)) { 460 break; 461 } 462 if (c == '\'' && (++currentIdx == pattern.length() || pattern.charAt(currentIdx) != '\'')) { 463 activeQuote = !activeQuote; 464 continue; 465 } 466 ++currentIdx; 467 sb.append(c); 468 } 469 470 if (activeQuote) { 471 throw new IllegalArgumentException("Unterminated quote"); 472 } 473 474 final String formatField = sb.toString(); 475 return new StrategyAndWidth(new CopyQuotedStrategy(formatField), formatField.length()); 476 } 477 } 478 479 /** 480 * A strategy that handles a time zone field in the parsing pattern 481 */ 482 static class TimeZoneStrategy extends PatternStrategy { 483 private static final class TzInfo { 484 final TimeZone zone; 485 final int dstOffset; 486 487 TzInfo(final TimeZone tz, final boolean useDst) { 488 zone = tz; 489 dstOffset = useDst ? tz.getDSTSavings() : 0; 490 } 491 492 @Override 493 public String toString() { 494 return "TzInfo [zone=" + zone + ", dstOffset=" + dstOffset + "]"; 495 } 496 } 497 private static final String RFC_822_TIME_ZONE = "[+-]\\d{4}"; 498 499 private static final String GMT_OPTION = TimeZones.GMT_ID + "[+-]\\d{1,2}:\\d{2}"; 500 /** 501 * Index of zone id 502 */ 503 private static final int ID = 0; 504 505 private final Locale locale; 506 507 private final Map<String, TzInfo> tzNames = new HashMap<>(); 508 509 /** 510 * Constructs a Strategy that parses a TimeZone 511 * 512 * @param locale The Locale 513 */ 514 TimeZoneStrategy(final Locale locale) { 515 this.locale = LocaleUtils.toLocale(locale); 516 517 final StringBuilder sb = new StringBuilder(); 518 sb.append("((?iu)" + RFC_822_TIME_ZONE + "|" + GMT_OPTION); 519 520 final Set<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE); 521 522 // Order is undefined. 523 // TODO Use of getZoneStrings() is discouraged per its Javadoc. 524 final String[][] zones = DateFormatSymbols.getInstance(locale).getZoneStrings(); 525 for (final String[] zoneNames : zones) { 526 // offset 0 is the time zone ID and is not localized 527 final String tzId = zoneNames[ID]; 528 if (tzId.equalsIgnoreCase(TimeZones.GMT_ID)) { 529 continue; 530 } 531 final TimeZone tz = TimeZone.getTimeZone(tzId); 532 // offset 1 is long standard name 533 // offset 2 is short standard name 534 final TzInfo standard = new TzInfo(tz, false); 535 TzInfo tzInfo = standard; 536 for (int i = 1; i < zoneNames.length; ++i) { 537 switch (i) { 538 case 3: // offset 3 is long daylight savings (or summertime) name 539 // offset 4 is the short summertime name 540 tzInfo = new TzInfo(tz, true); 541 break; 542 case 5: // offset 5 starts additional names, probably standard time 543 tzInfo = standard; 544 break; 545 default: 546 break; 547 } 548 final String zoneName = zoneNames[i]; 549 if (zoneName != null) { 550 final String key = zoneName.toLowerCase(locale); 551 // ignore the data associated with duplicates supplied in 552 // the additional names 553 if (sorted.add(key)) { 554 tzNames.put(key, tzInfo); 555 } 556 } 557 } 558 } 559 560 // Order is undefined. 561 for (final String tzId : TimeZone.getAvailableIDs()) { 562 if (tzId.equalsIgnoreCase(TimeZones.GMT_ID)) { 563 continue; 564 } 565 final TimeZone tz = TimeZone.getTimeZone(tzId); 566 final String zoneName = tz.getDisplayName(locale); 567 final String key = zoneName.toLowerCase(locale); 568 if (sorted.add(key)) { 569 tzNames.put(key, new TzInfo(tz, tz.observesDaylightTime())); 570 } 571 } 572 573 // order the regex alternatives with longer strings first, greedy 574 // match will ensure the longest string will be consumed 575 sorted.forEach(zoneName -> simpleQuote(sb.append('|'), zoneName)); 576 sb.append(")"); 577 createPattern(sb); 578 } 579 580 /** 581 * {@inheritDoc} 582 */ 583 @Override 584 void setCalendar(final FastDateParser parser, final Calendar calendar, final String timeZone) { 585 final TimeZone tz = FastTimeZone.getGmtTimeZone(timeZone); 586 if (tz != null) { 587 calendar.setTimeZone(tz); 588 } else { 589 final String lowerCase = timeZone.toLowerCase(locale); 590 TzInfo tzInfo = tzNames.get(lowerCase); 591 if (tzInfo == null) { 592 // match missing the optional trailing period 593 tzInfo = tzNames.get(lowerCase + '.'); 594 } 595 calendar.set(Calendar.DST_OFFSET, tzInfo.dstOffset); 596 calendar.set(Calendar.ZONE_OFFSET, tzInfo.zone.getRawOffset()); 597 } 598 } 599 600 /** 601 * Converts this instance to a handy debug string. 602 * 603 * @since 3.12.0 604 */ 605 @Override 606 public String toString() { 607 return "TimeZoneStrategy [locale=" + locale + ", tzNames=" + tzNames + ", pattern=" + pattern + "]"; 608 } 609 610 } 611 612 /** 613 * Required for serialization support. 614 * 615 * @see java.io.Serializable 616 */ 617 private static final long serialVersionUID = 3L; 618 619 static final Locale JAPANESE_IMPERIAL = new Locale("ja", "JP", "JP"); 620 621 /** 622 * comparator used to sort regex alternatives. Alternatives should be ordered longer first, and shorter last. ('february' before 'feb'). All entries must be 623 * lower-case by locale. 624 */ 625 private static final Comparator<String> LONGER_FIRST_LOWERCASE = Comparator.reverseOrder(); 626 627 // helper classes to parse the format string 628 629 @SuppressWarnings("unchecked") // OK because we are creating an array with no entries 630 private static final ConcurrentMap<Locale, Strategy>[] caches = new ConcurrentMap[Calendar.FIELD_COUNT]; 631 632 private static final Strategy ABBREVIATED_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR) { 633 /** 634 * {@inheritDoc} 635 */ 636 @Override 637 int modify(final FastDateParser parser, final int iValue) { 638 return iValue < 100 ? parser.adjustYear(iValue) : iValue; 639 } 640 }; 641 642 private static final Strategy NUMBER_MONTH_STRATEGY = new NumberStrategy(Calendar.MONTH) { 643 @Override 644 int modify(final FastDateParser parser, final int iValue) { 645 return iValue - 1; 646 } 647 }; 648 649 private static final Strategy LITERAL_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR); 650 651 private static final Strategy WEEK_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_YEAR); 652 653 private static final Strategy WEEK_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_MONTH); 654 655 private static final Strategy DAY_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.DAY_OF_YEAR); 656 657 private static final Strategy DAY_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_MONTH); 658 659 private static final Strategy DAY_OF_WEEK_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK) { 660 @Override 661 int modify(final FastDateParser parser, final int iValue) { 662 return iValue == 7 ? Calendar.SUNDAY : iValue + 1; 663 } 664 }; 665 666 private static final Strategy DAY_OF_WEEK_IN_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK_IN_MONTH); 667 668 private static final Strategy HOUR_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY); 669 670 private static final Strategy HOUR24_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY) { 671 @Override 672 int modify(final FastDateParser parser, final int iValue) { 673 return iValue == 24 ? 0 : iValue; 674 } 675 }; 676 677 private static final Strategy HOUR12_STRATEGY = new NumberStrategy(Calendar.HOUR) { 678 @Override 679 int modify(final FastDateParser parser, final int iValue) { 680 return iValue == 12 ? 0 : iValue; 681 } 682 }; 683 684 private static final Strategy HOUR_STRATEGY = new NumberStrategy(Calendar.HOUR); 685 686 private static final Strategy MINUTE_STRATEGY = new NumberStrategy(Calendar.MINUTE); 687 688 private static final Strategy SECOND_STRATEGY = new NumberStrategy(Calendar.SECOND); 689 690 // Support for strategies 691 692 private static final Strategy MILLISECOND_STRATEGY = new NumberStrategy(Calendar.MILLISECOND); 693 694 /** 695 * Gets the short and long values displayed for a field 696 * 697 * @param calendar The calendar to obtain the short and long values 698 * @param locale The locale of display names 699 * @param field The field of interest 700 * @param regex The regular expression to build 701 * @return The map of string display names to field values 702 */ 703 private static Map<String, Integer> appendDisplayNames(final Calendar calendar, final Locale locale, final int field, final StringBuilder regex) { 704 Objects.requireNonNull(calendar, "calendar"); 705 final Map<String, Integer> values = new HashMap<>(); 706 final Locale actualLocale = LocaleUtils.toLocale(locale); 707 final Map<String, Integer> displayNames = calendar.getDisplayNames(field, Calendar.ALL_STYLES, actualLocale); 708 final TreeSet<String> sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE); 709 displayNames.forEach((k, v) -> { 710 final String keyLc = k.toLowerCase(actualLocale); 711 if (sorted.add(keyLc)) { 712 values.put(keyLc, v); 713 } 714 }); 715 sorted.forEach(symbol -> simpleQuote(regex, symbol).append('|')); 716 return values; 717 } 718 719 /** 720 * Gets a cache of Strategies for a particular field 721 * 722 * @param field The Calendar field 723 * @return a cache of Locale to Strategy 724 */ 725 private static ConcurrentMap<Locale, Strategy> getCache(final int field) { 726 synchronized (caches) { 727 if (caches[field] == null) { 728 caches[field] = new ConcurrentHashMap<>(3); 729 } 730 return caches[field]; 731 } 732 } 733 734 private static boolean isFormatLetter(final char c) { 735 return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z'; 736 } 737 738 private static StringBuilder simpleQuote(final StringBuilder sb, final String value) { 739 for (int i = 0; i < value.length(); ++i) { 740 final char c = value.charAt(i); 741 switch (c) { 742 case '\\': 743 case '^': 744 case '$': 745 case '.': 746 case '|': 747 case '?': 748 case '*': 749 case '+': 750 case '(': 751 case ')': 752 case '[': 753 case '{': 754 sb.append('\\'); 755 default: 756 sb.append(c); 757 } 758 } 759 if (sb.charAt(sb.length() - 1) == '.') { 760 // trailing '.' is optional 761 sb.append('?'); 762 } 763 return sb; 764 } 765 766 /** Input pattern. */ 767 private final String pattern; 768 769 /** Input TimeZone. */ 770 private final TimeZone timeZone; 771 772 /** Input Locale. */ 773 private final Locale locale; 774 775 /** 776 * Century from Date. 777 */ 778 private final int century; 779 780 /** 781 * Start year from Date. 782 */ 783 private final int startYear; 784 785 /** Initialized from Calendar. */ 786 private transient List<StrategyAndWidth> patterns; 787 788 /** 789 * Constructs a new FastDateParser. 790 * 791 * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another variation of the factory methods of {@link FastDateFormat} to get a cached 792 * FastDateParser instance. 793 * 794 * @param pattern non-null {@link java.text.SimpleDateFormat} compatible pattern 795 * @param timeZone non-null time zone to use 796 * @param locale non-null locale 797 */ 798 protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale) { 799 this(pattern, timeZone, locale, null); 800 } 801 802 /** 803 * Constructs a new FastDateParser. 804 * 805 * @param pattern non-null {@link java.text.SimpleDateFormat} compatible pattern 806 * @param timeZone non-null time zone to use 807 * @param locale locale, null maps to the default Locale. 808 * @param centuryStart The start of the century for 2 digit year parsing 809 * 810 * @since 3.5 811 */ 812 protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) { 813 this.pattern = Objects.requireNonNull(pattern, "pattern"); 814 this.timeZone = Objects.requireNonNull(timeZone, "timeZone"); 815 this.locale = LocaleUtils.toLocale(locale); 816 817 final Calendar definingCalendar = Calendar.getInstance(timeZone, this.locale); 818 819 final int centuryStartYear; 820 if (centuryStart != null) { 821 definingCalendar.setTime(centuryStart); 822 centuryStartYear = definingCalendar.get(Calendar.YEAR); 823 } else if (this.locale.equals(JAPANESE_IMPERIAL)) { 824 centuryStartYear = 0; 825 } else { 826 // from 80 years ago to 20 years from now 827 definingCalendar.setTime(new Date()); 828 centuryStartYear = definingCalendar.get(Calendar.YEAR) - 80; 829 } 830 century = centuryStartYear / 100 * 100; 831 startYear = centuryStartYear - century; 832 833 init(definingCalendar); 834 } 835 836 /** 837 * Adjusts dates to be within appropriate century 838 * 839 * @param twoDigitYear The year to adjust 840 * @return A value between centuryStart(inclusive) to centuryStart+100(exclusive) 841 */ 842 private int adjustYear(final int twoDigitYear) { 843 final int trial = century + twoDigitYear; 844 return twoDigitYear >= startYear ? trial : trial + 100; 845 } 846 847 // Basics 848 /** 849 * Compares another object for equality with this object. 850 * 851 * @param obj the object to compare to 852 * @return {@code true}if equal to this instance 853 */ 854 @Override 855 public boolean equals(final Object obj) { 856 if (!(obj instanceof FastDateParser)) { 857 return false; 858 } 859 final FastDateParser other = (FastDateParser) obj; 860 return pattern.equals(other.pattern) && timeZone.equals(other.timeZone) && locale.equals(other.locale); 861 } 862 863 /* 864 * (non-Javadoc) 865 * 866 * @see org.apache.commons.lang3.time.DateParser#getLocale() 867 */ 868 @Override 869 public Locale getLocale() { 870 return locale; 871 } 872 873 /** 874 * Constructs a Strategy that parses a Text field 875 * 876 * @param field The Calendar field 877 * @param definingCalendar The calendar to obtain the short and long values 878 * @return a TextStrategy for the field and Locale 879 */ 880 private Strategy getLocaleSpecificStrategy(final int field, final Calendar definingCalendar) { 881 final ConcurrentMap<Locale, Strategy> cache = getCache(field); 882 return cache.computeIfAbsent(locale, 883 k -> field == Calendar.ZONE_OFFSET ? new TimeZoneStrategy(locale) : new CaseInsensitiveTextStrategy(field, definingCalendar, locale)); 884 } 885 // Accessors 886 /* 887 * (non-Javadoc) 888 * 889 * @see org.apache.commons.lang3.time.DateParser#getPattern() 890 */ 891 @Override 892 public String getPattern() { 893 return pattern; 894 } 895 /** 896 * Gets a Strategy given a field from a SimpleDateFormat pattern 897 * 898 * @param f A sub-sequence of the SimpleDateFormat pattern 899 * @param width formatting width 900 * @param definingCalendar The calendar to obtain the short and long values 901 * @return The Strategy that will handle parsing for the field 902 */ 903 private Strategy getStrategy(final char f, final int width, final Calendar definingCalendar) { 904 switch (f) { 905 default: 906 throw new IllegalArgumentException("Format '" + f + "' not supported"); 907 case 'D': 908 return DAY_OF_YEAR_STRATEGY; 909 case 'E': 910 return getLocaleSpecificStrategy(Calendar.DAY_OF_WEEK, definingCalendar); 911 case 'F': 912 return DAY_OF_WEEK_IN_MONTH_STRATEGY; 913 case 'G': 914 return getLocaleSpecificStrategy(Calendar.ERA, definingCalendar); 915 case 'H': // Hour in day (0-23) 916 return HOUR_OF_DAY_STRATEGY; 917 case 'K': // Hour in am/pm (0-11) 918 return HOUR_STRATEGY; 919 case 'M': 920 case 'L': 921 return width >= 3 ? getLocaleSpecificStrategy(Calendar.MONTH, definingCalendar) : NUMBER_MONTH_STRATEGY; 922 case 'S': 923 return MILLISECOND_STRATEGY; 924 case 'W': 925 return WEEK_OF_MONTH_STRATEGY; 926 case 'a': 927 return getLocaleSpecificStrategy(Calendar.AM_PM, definingCalendar); 928 case 'd': 929 return DAY_OF_MONTH_STRATEGY; 930 case 'h': // Hour in am/pm (1-12), i.e. midday/midnight is 12, not 0 931 return HOUR12_STRATEGY; 932 case 'k': // Hour in day (1-24), i.e. midnight is 24, not 0 933 return HOUR24_OF_DAY_STRATEGY; 934 case 'm': 935 return MINUTE_STRATEGY; 936 case 's': 937 return SECOND_STRATEGY; 938 case 'u': 939 return DAY_OF_WEEK_STRATEGY; 940 case 'w': 941 return WEEK_OF_YEAR_STRATEGY; 942 case 'y': 943 case 'Y': 944 return width > 2 ? LITERAL_YEAR_STRATEGY : ABBREVIATED_YEAR_STRATEGY; 945 case 'X': 946 return ISO8601TimeZoneStrategy.getStrategy(width); 947 case 'Z': 948 if (width == 2) { 949 return ISO8601TimeZoneStrategy.ISO_8601_3_STRATEGY; 950 } 951 //$FALL-THROUGH$ 952 case 'z': 953 return getLocaleSpecificStrategy(Calendar.ZONE_OFFSET, definingCalendar); 954 } 955 } 956 /* 957 * (non-Javadoc) 958 * 959 * @see org.apache.commons.lang3.time.DateParser#getTimeZone() 960 */ 961 @Override 962 public TimeZone getTimeZone() { 963 return timeZone; 964 } 965 /** 966 * Returns a hash code compatible with equals. 967 * 968 * @return a hash code compatible with equals 969 */ 970 @Override 971 public int hashCode() { 972 return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode()); 973 } 974 /** 975 * Initializes derived fields from defining fields. This is called from constructor and from readObject (de-serialization) 976 * 977 * @param definingCalendar the {@link java.util.Calendar} instance used to initialize this FastDateParser 978 */ 979 private void init(final Calendar definingCalendar) { 980 patterns = new ArrayList<>(); 981 982 final StrategyParser strategyParser = new StrategyParser(definingCalendar); 983 for (;;) { 984 final StrategyAndWidth field = strategyParser.getNextStrategy(); 985 if (field == null) { 986 break; 987 } 988 patterns.add(field); 989 } 990 } 991 992 /* 993 * (non-Javadoc) 994 * 995 * @see org.apache.commons.lang3.time.DateParser#parse(String) 996 */ 997 @Override 998 public Date parse(final String source) throws ParseException { 999 final ParsePosition pp = new ParsePosition(0); 1000 final Date date = parse(source, pp); 1001 if (date == null) { 1002 // Add a note regarding supported date range 1003 if (locale.equals(JAPANESE_IMPERIAL)) { 1004 throw new ParseException("(The " + locale + " locale does not support dates before 1868 AD)\nUnparseable date: \"" + source, 1005 pp.getErrorIndex()); 1006 } 1007 throw new ParseException("Unparseable date: " + source, pp.getErrorIndex()); 1008 } 1009 return date; 1010 } 1011 /** 1012 * This implementation updates the ParsePosition if the parse succeeds. However, it sets the error index to the position before the failed field unlike the 1013 * method {@link java.text.SimpleDateFormat#parse(String, ParsePosition)} which sets the error index to after the failed field. 1014 * <p> 1015 * To determine if the parse has succeeded, the caller must check if the current parse position given by {@link ParsePosition#getIndex()} has been updated. 1016 * If the input buffer has been fully parsed, then the index will point to just after the end of the input buffer. 1017 * 1018 * @see org.apache.commons.lang3.time.DateParser#parse(String, java.text.ParsePosition) 1019 */ 1020 @Override 1021 public Date parse(final String source, final ParsePosition pos) { 1022 // timing tests indicate getting new instance is 19% faster than cloning 1023 final Calendar cal = Calendar.getInstance(timeZone, locale); 1024 cal.clear(); 1025 1026 return parse(source, pos, cal) ? cal.getTime() : null; 1027 } 1028 /** 1029 * Parses a formatted date string according to the format. Updates the Calendar with parsed fields. Upon success, the ParsePosition index is updated to 1030 * indicate how much of the source text was consumed. Not all source text needs to be consumed. Upon parse failure, ParsePosition error index is updated to 1031 * the offset of the source text which does not match the supplied format. 1032 * 1033 * @param source The text to parse. 1034 * @param pos On input, the position in the source to start parsing, on output, updated position. 1035 * @param calendar The calendar into which to set parsed fields. 1036 * @return true, if source has been parsed (pos parsePosition is updated); otherwise false (and pos errorIndex is updated) 1037 * @throws IllegalArgumentException when Calendar has been set to be not lenient, and a parsed field is out of range. 1038 */ 1039 @Override 1040 public boolean parse(final String source, final ParsePosition pos, final Calendar calendar) { 1041 final ListIterator<StrategyAndWidth> lt = patterns.listIterator(); 1042 while (lt.hasNext()) { 1043 final StrategyAndWidth strategyAndWidth = lt.next(); 1044 final int maxWidth = strategyAndWidth.getMaxWidth(lt); 1045 if (!strategyAndWidth.strategy.parse(this, calendar, source, pos, maxWidth)) { 1046 return false; 1047 } 1048 } 1049 return true; 1050 } 1051 1052 /* 1053 * (non-Javadoc) 1054 * 1055 * @see org.apache.commons.lang3.time.DateParser#parseObject(String) 1056 */ 1057 @Override 1058 public Object parseObject(final String source) throws ParseException { 1059 return parse(source); 1060 } 1061 1062 /* 1063 * (non-Javadoc) 1064 * 1065 * @see org.apache.commons.lang3.time.DateParser#parseObject(String, java.text.ParsePosition) 1066 */ 1067 @Override 1068 public Object parseObject(final String source, final ParsePosition pos) { 1069 return parse(source, pos); 1070 } 1071 // Serializing 1072 /** 1073 * Creates the object after serialization. This implementation reinitializes the transient properties. 1074 * 1075 * @param in ObjectInputStream from which the object is being deserialized. 1076 * @throws IOException if there is an IO issue. 1077 * @throws ClassNotFoundException if a class cannot be found. 1078 */ 1079 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { 1080 in.defaultReadObject(); 1081 1082 final Calendar definingCalendar = Calendar.getInstance(timeZone, locale); 1083 init(definingCalendar); 1084 } 1085 /** 1086 * Gets a string version of this formatter. 1087 * 1088 * @return a debugging string 1089 */ 1090 @Override 1091 public String toString() { 1092 return "FastDateParser[" + pattern + ", " + locale + ", " + timeZone.getID() + "]"; 1093 } 1094 /** 1095 * Converts all state of this instance to a String handy for debugging. 1096 * 1097 * @return a string. 1098 * @since 3.12.0 1099 */ 1100 public String toStringAll() { 1101 return "FastDateParser [pattern=" + pattern + ", timeZone=" + timeZone + ", locale=" + locale + ", century=" + century + ", startYear=" + startYear 1102 + ", patterns=" + patterns + "]"; 1103 } 1104}