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