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