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