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