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