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