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