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