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