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