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