1/* GENERATED SOURCE. DO NOT MODIFY. */ 2// © 2016 and later: Unicode, Inc. and others. 3// License & terms of use: http://www.unicode.org/copyright.html#License 4/* 5 ******************************************************************************* 6 * Copyright (C) 2007-2015, International Business Machines Corporation and * 7 * others. All Rights Reserved. * 8 ******************************************************************************* 9 */ 10package android.icu.util; 11 12import java.io.BufferedWriter; 13import java.io.IOException; 14import java.io.Reader; 15import java.io.Writer; 16import java.util.ArrayList; 17import java.util.Date; 18import java.util.LinkedList; 19import java.util.List; 20import java.util.MissingResourceException; 21import java.util.StringTokenizer; 22 23import android.icu.impl.Grego; 24 25/** 26 * <code>VTimeZone</code> is a class implementing RFC2445 VTIMEZONE. You can create a 27 * <code>VTimeZone</code> instance from a time zone ID supported by <code>TimeZone</code>. 28 * With the <code>VTimeZone</code> instance created from the ID, you can write out the rule 29 * in RFC2445 VTIMEZONE format. Also, you can create a <code>VTimeZone</code> instance 30 * from RFC2445 VTIMEZONE data stream, which allows you to calculate time 31 * zone offset by the rules defined by the data.<br><br> 32 * 33 * Note: The consumer of this class reading or writing VTIMEZONE data is responsible to 34 * decode or encode Non-ASCII text. Methods reading/writing VTIMEZONE data in this class 35 * do nothing with MIME encoding. 36 * 37 * @hide Only a subset of ICU is exposed in Android 38 */ 39public class VTimeZone extends BasicTimeZone { 40 41 private static final long serialVersionUID = -6851467294127795902L; 42 43 /** 44 * Create a <code>VTimeZone</code> instance by the time zone ID. 45 * 46 * @param tzid The time zone ID, such as America/New_York 47 * @return A <code>VTimeZone</code> initialized by the time zone ID, or null 48 * when the ID is unknown. 49 */ 50 public static VTimeZone create(String tzid) { 51 BasicTimeZone basicTimeZone = TimeZone.getFrozenICUTimeZone(tzid, true); 52 if (basicTimeZone == null) { 53 return null; 54 } 55 VTimeZone vtz = new VTimeZone(tzid); 56 vtz.tz = (BasicTimeZone) basicTimeZone.cloneAsThawed(); 57 vtz.olsonzid = vtz.tz.getID(); 58 59 return vtz; 60 } 61 62 /** 63 * Create a <code>VTimeZone</code> instance by RFC2445 VTIMEZONE data. 64 * 65 * @param reader The Reader for VTIMEZONE data input stream 66 * @return A <code>VTimeZone</code> initialized by the VTIMEZONE data or 67 * null if failed to load the rule from the VTIMEZONE data. 68 */ 69 public static VTimeZone create(Reader reader) { 70 VTimeZone vtz = new VTimeZone(); 71 if (vtz.load(reader)) { 72 return vtz; 73 } 74 return null; 75 } 76 77 /** 78 * {@inheritDoc} 79 */ 80 @Override 81 public int getOffset(int era, int year, int month, int day, int dayOfWeek, 82 int milliseconds) { 83 return tz.getOffset(era, year, month, day, dayOfWeek, milliseconds); 84 } 85 86 /** 87 * {@inheritDoc} 88 */ 89 @Override 90 public void getOffset(long date, boolean local, int[] offsets) { 91 tz.getOffset(date, local, offsets); 92 } 93 94 /** 95 * {@inheritDoc} 96 * @deprecated This API is ICU internal only. 97 * @hide draft / provisional / internal are hidden on Android 98 */ 99 @Deprecated 100 @Override 101 public void getOffsetFromLocal(long date, 102 int nonExistingTimeOpt, int duplicatedTimeOpt, int[] offsets) { 103 tz.getOffsetFromLocal(date, nonExistingTimeOpt, duplicatedTimeOpt, offsets); 104 } 105 106 /** 107 * {@inheritDoc} 108 */ 109 @Override 110 public int getRawOffset() { 111 return tz.getRawOffset(); 112 } 113 114 /** 115 * {@inheritDoc} 116 */ 117 @Override 118 public boolean inDaylightTime(Date date) { 119 return tz.inDaylightTime(date); 120 } 121 122 /** 123 * {@inheritDoc} 124 */ 125 @Override 126 public void setRawOffset(int offsetMillis) { 127 if (isFrozen()) { 128 throw new UnsupportedOperationException("Attempt to modify a frozen VTimeZone instance."); 129 } 130 tz.setRawOffset(offsetMillis); 131 } 132 133 /** 134 * {@inheritDoc} 135 */ 136 @Override 137 public boolean useDaylightTime() { 138 return tz.useDaylightTime(); 139 } 140 141 /** 142 * {@inheritDoc} 143 */ 144 @Override 145 public boolean observesDaylightTime() { 146 return tz.observesDaylightTime(); 147 } 148 149 /** 150 * {@inheritDoc} 151 */ 152 @Override 153 public boolean hasSameRules(TimeZone other) { 154 if (this == other) { 155 return true; 156 } 157 if (other instanceof VTimeZone) { 158 return tz.hasSameRules(((VTimeZone)other).tz); 159 } 160 return tz.hasSameRules(other); 161 } 162 163 /** 164 * Gets the RFC2445 TZURL property value. When a <code>VTimeZone</code> instance was created from 165 * VTIMEZONE data, the value is set by the TZURL property value in the data. Otherwise, 166 * the initial value is null. 167 * 168 * @return The RFC2445 TZURL property value 169 */ 170 public String getTZURL() { 171 return tzurl; 172 } 173 174 /** 175 * Sets the RFC2445 TZURL property value. 176 * 177 * @param url The TZURL property value. 178 */ 179 public void setTZURL(String url) { 180 if (isFrozen()) { 181 throw new UnsupportedOperationException("Attempt to modify a frozen VTimeZone instance."); 182 } 183 tzurl = url; 184 } 185 186 /** 187 * Gets the RFC2445 LAST-MODIFIED property value. When a <code>VTimeZone</code> instance was created 188 * from VTIMEZONE data, the value is set by the LAST-MODIFIED property value in the data. 189 * Otherwise, the initial value is null. 190 * 191 * @return The Date represents the RFC2445 LAST-MODIFIED date. 192 */ 193 public Date getLastModified() { 194 return lastmod; 195 } 196 197 /** 198 * Sets the date used for RFC2445 LAST-MODIFIED property value. 199 * 200 * @param date The <code>Date</code> object represents the date for RFC2445 LAST-MODIFIED property value. 201 */ 202 public void setLastModified(Date date) { 203 if (isFrozen()) { 204 throw new UnsupportedOperationException("Attempt to modify a frozen VTimeZone instance."); 205 } 206 lastmod = date; 207 } 208 209 /** 210 * Writes RFC2445 VTIMEZONE data for this time zone 211 * 212 * @param writer A <code>Writer</code> used for the output 213 * @throws IOException If there were problems creating a buffered writer or writing to it. 214 */ 215 public void write(Writer writer) throws IOException { 216 BufferedWriter bw = new BufferedWriter(writer); 217 if (vtzlines != null) { 218 for (String line : vtzlines) { 219 if (line.startsWith(ICAL_TZURL + COLON)) { 220 if (tzurl != null) { 221 bw.write(ICAL_TZURL); 222 bw.write(COLON); 223 bw.write(tzurl); 224 bw.write(NEWLINE); 225 } 226 } else if (line.startsWith(ICAL_LASTMOD + COLON)) { 227 if (lastmod != null) { 228 bw.write(ICAL_LASTMOD); 229 bw.write(COLON); 230 bw.write(getUTCDateTimeString(lastmod.getTime())); 231 bw.write(NEWLINE); 232 } 233 } else { 234 bw.write(line); 235 bw.write(NEWLINE); 236 } 237 } 238 bw.flush(); 239 } else { 240 String[] customProperties = null; 241 if (olsonzid != null && ICU_TZVERSION != null) { 242 customProperties = new String[1]; 243 customProperties[0] = ICU_TZINFO_PROP + COLON + olsonzid + "[" + ICU_TZVERSION + "]"; 244 } 245 writeZone(writer, tz, customProperties); 246 } 247 } 248 249 /** 250 * Writes RFC2445 VTIMEZONE data applicable for dates after 251 * the specified start time. 252 * 253 * @param writer The <code>Writer</code> used for the output 254 * @param start The start time 255 * 256 * @throws IOException If there were problems reading and writing to the writer. 257 */ 258 public void write(Writer writer, long start) throws IOException { 259 // Extract rules applicable to dates after the start time 260 TimeZoneRule[] rules = tz.getTimeZoneRules(start); 261 262 // Create a RuleBasedTimeZone with the subset rule 263 RuleBasedTimeZone rbtz = new RuleBasedTimeZone(tz.getID(), (InitialTimeZoneRule)rules[0]); 264 for (int i = 1; i < rules.length; i++) { 265 rbtz.addTransitionRule(rules[i]); 266 } 267 String[] customProperties = null; 268 if (olsonzid != null && ICU_TZVERSION != null) { 269 customProperties = new String[1]; 270 customProperties[0] = ICU_TZINFO_PROP + COLON + olsonzid + "[" + ICU_TZVERSION + 271 "/Partial@" + start + "]"; 272 } 273 writeZone(writer, rbtz, customProperties); 274 } 275 276 /** 277 * Writes RFC2445 VTIMEZONE data applicable near the specified date. 278 * Some common iCalendar implementations can only handle a single time 279 * zone property or a pair of standard and daylight time properties using 280 * BYDAY rule with day of week (such as BYDAY=1SUN). This method produce 281 * the VTIMEZONE data which can be handled these implementations. The rules 282 * produced by this method can be used only for calculating time zone offset 283 * around the specified date. 284 * 285 * @param writer The <code>Writer</code> used for the output 286 * @param time The date 287 * 288 * @throws IOException If there were problems reading or writing to the writer. 289 */ 290 public void writeSimple(Writer writer, long time) throws IOException { 291 // Extract simple rules 292 TimeZoneRule[] rules = tz.getSimpleTimeZoneRulesNear(time); 293 294 // Create a RuleBasedTimeZone with the subset rule 295 RuleBasedTimeZone rbtz = new RuleBasedTimeZone(tz.getID(), (InitialTimeZoneRule)rules[0]); 296 for (int i = 1; i < rules.length; i++) { 297 rbtz.addTransitionRule(rules[i]); 298 } 299 String[] customProperties = null; 300 if (olsonzid != null && ICU_TZVERSION != null) { 301 customProperties = new String[1]; 302 customProperties[0] = ICU_TZINFO_PROP + COLON + olsonzid + "[" + ICU_TZVERSION + 303 "/Simple@" + time + "]"; 304 } 305 writeZone(writer, rbtz, customProperties); 306 } 307 308 // BasicTimeZone methods 309 310 /** 311 * {@inheritDoc} 312 */ 313 @Override 314 public TimeZoneTransition getNextTransition(long base, boolean inclusive) { 315 return tz.getNextTransition(base, inclusive); 316 } 317 318 /** 319 * {@inheritDoc} 320 */ 321 @Override 322 public TimeZoneTransition getPreviousTransition(long base, boolean inclusive) { 323 return tz.getPreviousTransition(base, inclusive); 324 } 325 326 /** 327 * {@inheritDoc} 328 */ 329 @Override 330 public boolean hasEquivalentTransitions(TimeZone other, long start, long end) { 331 if (this == other) { 332 return true; 333 } 334 return tz.hasEquivalentTransitions(other, start, end); 335 } 336 337 /** 338 * {@inheritDoc} 339 */ 340 @Override 341 public TimeZoneRule[] getTimeZoneRules() { 342 return tz.getTimeZoneRules(); 343 } 344 345 /** 346 * {@inheritDoc} 347 */ 348 @Override 349 public TimeZoneRule[] getTimeZoneRules(long start) { 350 return tz.getTimeZoneRules(start); 351 } 352 353 /** 354 * {@inheritDoc} 355 */ 356 @Override 357 public Object clone() { 358 if (isFrozen()) { 359 return this; 360 } 361 return cloneAsThawed(); 362 } 363 364 // private stuff ------------------------------------------------------ 365 366 private BasicTimeZone tz; 367 private List<String> vtzlines; 368 private String olsonzid = null; 369 private String tzurl = null; 370 private Date lastmod = null; 371 372 private static String ICU_TZVERSION; 373 private static final String ICU_TZINFO_PROP = "X-TZINFO"; 374 375 // Default DST savings 376 private static final int DEF_DSTSAVINGS = 60*60*1000; // 1 hour 377 378 // Default time start 379 private static final long DEF_TZSTARTTIME = 0; 380 381 // minimum/max 382 private static final long MIN_TIME = Long.MIN_VALUE; 383 private static final long MAX_TIME = Long.MAX_VALUE; 384 385 // Symbol characters used by RFC2445 VTIMEZONE 386 private static final String COLON = ":"; 387 private static final String SEMICOLON = ";"; 388 private static final String EQUALS_SIGN = "="; 389 private static final String COMMA = ","; 390 private static final String NEWLINE = "\r\n"; // CRLF 391 392 // RFC2445 VTIMEZONE tokens 393 private static final String ICAL_BEGIN_VTIMEZONE = "BEGIN:VTIMEZONE"; 394 private static final String ICAL_END_VTIMEZONE = "END:VTIMEZONE"; 395 private static final String ICAL_BEGIN = "BEGIN"; 396 private static final String ICAL_END = "END"; 397 private static final String ICAL_VTIMEZONE = "VTIMEZONE"; 398 private static final String ICAL_TZID = "TZID"; 399 private static final String ICAL_STANDARD = "STANDARD"; 400 private static final String ICAL_DAYLIGHT = "DAYLIGHT"; 401 private static final String ICAL_DTSTART = "DTSTART"; 402 private static final String ICAL_TZOFFSETFROM = "TZOFFSETFROM"; 403 private static final String ICAL_TZOFFSETTO = "TZOFFSETTO"; 404 private static final String ICAL_RDATE = "RDATE"; 405 private static final String ICAL_RRULE = "RRULE"; 406 private static final String ICAL_TZNAME = "TZNAME"; 407 private static final String ICAL_TZURL = "TZURL"; 408 private static final String ICAL_LASTMOD = "LAST-MODIFIED"; 409 410 private static final String ICAL_FREQ = "FREQ"; 411 private static final String ICAL_UNTIL = "UNTIL"; 412 private static final String ICAL_YEARLY = "YEARLY"; 413 private static final String ICAL_BYMONTH = "BYMONTH"; 414 private static final String ICAL_BYDAY = "BYDAY"; 415 private static final String ICAL_BYMONTHDAY = "BYMONTHDAY"; 416 417 private static final String[] ICAL_DOW_NAMES = 418 {"SU", "MO", "TU", "WE", "TH", "FR", "SA"}; 419 420 // Month length in regular year 421 private static final int[] MONTHLENGTH = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; 422 423 static { 424 // Initialize ICU_TZVERSION 425 try { 426 ICU_TZVERSION = TimeZone.getTZDataVersion(); 427 } catch (MissingResourceException e) { 428 ///CLOVER:OFF 429 ICU_TZVERSION = null; 430 ///CLOVER:ON 431 } 432 } 433 434 /* Hide the constructor */ 435 private VTimeZone() { 436 } 437 438 private VTimeZone(String tzid) { 439 super(tzid); 440 } 441 442 /* 443 * Read the input stream to locate the VTIMEZONE block and 444 * parse the contents to initialize this VTimeZone object. 445 * The reader skips other RFC2445 message headers. After 446 * the parse is completed, the reader points at the beginning 447 * of the header field just after the end of VTIMEZONE block. 448 * When VTIMEZONE block is found and this object is successfully 449 * initialized by the rules described in the data, this method 450 * returns true. Otherwise, returns false. 451 */ 452 private boolean load(Reader reader) { 453 // Read VTIMEZONE block into string array 454 try { 455 vtzlines = new LinkedList<String>(); 456 boolean eol = false; 457 boolean start = false; 458 boolean success = false; 459 StringBuilder line = new StringBuilder(); 460 while (true) { 461 int ch = reader.read(); 462 if (ch == -1) { 463 // end of file 464 if (start && line.toString().startsWith(ICAL_END_VTIMEZONE)) { 465 vtzlines.add(line.toString()); 466 success = true; 467 } 468 break; 469 } 470 if (ch == 0x0D) { 471 // CR, must be followed by LF by the definition in RFC2445 472 continue; 473 } 474 475 if (eol) { 476 if (ch != 0x09 && ch != 0x20) { 477 // NOT followed by TAB/SP -> new line 478 if (start) { 479 if (line.length() > 0) { 480 vtzlines.add(line.toString()); 481 } 482 } 483 line.setLength(0); 484 if (ch != 0x0A) { 485 line.append((char)ch); 486 } 487 } 488 eol = false; 489 } else { 490 if (ch == 0x0A) { 491 // LF 492 eol = true; 493 if (start) { 494 if (line.toString().startsWith(ICAL_END_VTIMEZONE)) { 495 vtzlines.add(line.toString()); 496 success = true; 497 break; 498 } 499 } else { 500 if (line.toString().startsWith(ICAL_BEGIN_VTIMEZONE)) { 501 vtzlines.add(line.toString()); 502 line.setLength(0); 503 start = true; 504 eol = false; 505 } 506 } 507 } else { 508 line.append((char)ch); 509 } 510 } 511 } 512 if (!success) { 513 return false; 514 } 515 } catch (IOException ioe) { 516 ///CLOVER:OFF 517 return false; 518 ///CLOVER:ON 519 } 520 return parse(); 521 } 522 523 // parser state 524 private static final int INI = 0; // Initial state 525 private static final int VTZ = 1; // In VTIMEZONE 526 private static final int TZI = 2; // In STANDARD or DAYLIGHT 527 private static final int ERR = 3; // Error state 528 529 /* 530 * Parse VTIMEZONE data and create a RuleBasedTimeZone 531 */ 532 private boolean parse() { 533 ///CLOVER:OFF 534 if (vtzlines == null || vtzlines.size() == 0) { 535 return false; 536 } 537 ///CLOVER:ON 538 539 // timezone ID 540 String tzid = null; 541 542 int state = INI; 543 boolean dst = false; // current zone type 544 String from = null; // current zone from offset 545 String to = null; // current zone offset 546 String tzname = null; // current zone name 547 String dtstart = null; // current zone starts 548 boolean isRRULE = false; // true if the rule is described by RRULE 549 List<String> dates = null; // list of RDATE or RRULE strings 550 List<TimeZoneRule> rules = new ArrayList<TimeZoneRule>(); // rule list 551 int initialRawOffset = 0; // initial offset 552 int initialDSTSavings = 0; // initial offset 553 long firstStart = MAX_TIME; // the earliest rule start time 554 555 for (String line : vtzlines) { 556 int valueSep = line.indexOf(COLON); 557 if (valueSep < 0) { 558 continue; 559 } 560 String name = line.substring(0, valueSep); 561 String value = line.substring(valueSep + 1); 562 563 switch (state) { 564 case INI: 565 if (name.equals(ICAL_BEGIN) && value.equals(ICAL_VTIMEZONE)) { 566 state = VTZ; 567 } 568 break; 569 case VTZ: 570 if (name.equals(ICAL_TZID)) { 571 tzid = value; 572 } else if (name.equals(ICAL_TZURL)) { 573 tzurl = value; 574 } else if (name.equals(ICAL_LASTMOD)) { 575 // Always in 'Z' format, so the offset argument for the parse method 576 // can be any value. 577 lastmod = new Date(parseDateTimeString(value, 0)); 578 } else if (name.equals(ICAL_BEGIN)) { 579 boolean isDST = value.equals(ICAL_DAYLIGHT); 580 if (value.equals(ICAL_STANDARD) || isDST) { 581 // tzid must be ready at this point 582 if (tzid == null) { 583 state = ERR; 584 break; 585 } 586 // initialize current zone properties 587 dates = null; 588 isRRULE = false; 589 from = null; 590 to = null; 591 tzname = null; 592 dst = isDST; 593 state = TZI; 594 } else { 595 // BEGIN property other than STANDARD/DAYLIGHT 596 // must not be there. 597 state = ERR; 598 break; 599 } 600 } else if (name.equals(ICAL_END) /* && value.equals(ICAL_VTIMEZONE) */) { 601 break; 602 } 603 break; 604 605 case TZI: 606 if (name.equals(ICAL_DTSTART)) { 607 dtstart = value; 608 } else if (name.equals(ICAL_TZNAME)) { 609 tzname = value; 610 } else if (name.equals(ICAL_TZOFFSETFROM)) { 611 from = value; 612 } else if (name.equals(ICAL_TZOFFSETTO)) { 613 to = value; 614 } else if (name.equals(ICAL_RDATE)) { 615 // RDATE mixed with RRULE is not supported 616 if (isRRULE) { 617 state = ERR; 618 break; 619 } 620 if (dates == null) { 621 dates = new LinkedList<String>(); 622 } 623 // RDATE value may contain multiple date delimited 624 // by comma 625 StringTokenizer st = new StringTokenizer(value, COMMA); 626 while (st.hasMoreTokens()) { 627 String date = st.nextToken(); 628 dates.add(date); 629 } 630 } else if (name.equals(ICAL_RRULE)) { 631 // RRULE mixed with RDATE is not supported 632 if (!isRRULE && dates != null) { 633 state = ERR; 634 break; 635 } else if (dates == null) { 636 dates = new LinkedList<String>(); 637 } 638 isRRULE = true; 639 dates.add(value); 640 } else if (name.equals(ICAL_END)) { 641 // Mandatory properties 642 if (dtstart == null || from == null || to == null) { 643 state = ERR; 644 break; 645 } 646 // if tzname is not available, create one from tzid 647 if (tzname == null) { 648 tzname = getDefaultTZName(tzid, dst); 649 } 650 651 // create a time zone rule 652 TimeZoneRule rule = null; 653 int fromOffset = 0; 654 int toOffset = 0; 655 int rawOffset = 0; 656 int dstSavings = 0; 657 long start = 0; 658 try { 659 // Parse TZOFFSETFROM/TZOFFSETTO 660 fromOffset = offsetStrToMillis(from); 661 toOffset = offsetStrToMillis(to); 662 663 if (dst) { 664 // If daylight, use the previous offset as rawoffset if positive 665 if (toOffset - fromOffset > 0) { 666 rawOffset = fromOffset; 667 dstSavings = toOffset - fromOffset; 668 } else { 669 // This is rare case.. just use 1 hour DST savings 670 rawOffset = toOffset - DEF_DSTSAVINGS; 671 dstSavings = DEF_DSTSAVINGS; 672 } 673 } else { 674 rawOffset = toOffset; 675 dstSavings = 0; 676 } 677 678 // start time 679 start = parseDateTimeString(dtstart, fromOffset); 680 681 // Create the rule 682 Date actualStart = null; 683 if (isRRULE) { 684 rule = createRuleByRRULE(tzname, rawOffset, dstSavings, start, dates, fromOffset); 685 } else { 686 rule = createRuleByRDATE(tzname, rawOffset, dstSavings, start, dates, fromOffset); 687 } 688 if (rule != null) { 689 actualStart = rule.getFirstStart(fromOffset, 0); 690 if (actualStart.getTime() < firstStart) { 691 // save from offset information for the earliest rule 692 firstStart = actualStart.getTime(); 693 // If this is STD, assume the time before this transtion 694 // is DST when the difference is 1 hour. This might not be 695 // accurate, but VTIMEZONE data does not have such info. 696 if (dstSavings > 0) { 697 initialRawOffset = fromOffset; 698 initialDSTSavings = 0; 699 } else { 700 if (fromOffset - toOffset == DEF_DSTSAVINGS) { 701 initialRawOffset = fromOffset - DEF_DSTSAVINGS; 702 initialDSTSavings = DEF_DSTSAVINGS; 703 } else { 704 initialRawOffset = fromOffset; 705 initialDSTSavings = 0; 706 } 707 } 708 } 709 } 710 } catch (IllegalArgumentException iae) { 711 // bad format - rule == null.. 712 } 713 714 if (rule == null) { 715 state = ERR; 716 break; 717 } 718 rules.add(rule); 719 state = VTZ; 720 } 721 break; 722 } 723 724 if (state == ERR) { 725 vtzlines = null; 726 return false; 727 } 728 } 729 730 // Must have at least one rule 731 if (rules.size() == 0) { 732 return false; 733 } 734 735 // Create a initial rule 736 InitialTimeZoneRule initialRule = new InitialTimeZoneRule(getDefaultTZName(tzid, false), 737 initialRawOffset, initialDSTSavings); 738 739 // Finally, create the RuleBasedTimeZone 740 RuleBasedTimeZone rbtz = new RuleBasedTimeZone(tzid, initialRule); 741 742 int finalRuleIdx = -1; 743 int finalRuleCount = 0; 744 for (int i = 0; i < rules.size(); i++) { 745 TimeZoneRule r = rules.get(i); 746 if (r instanceof AnnualTimeZoneRule) { 747 if (((AnnualTimeZoneRule)r).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) { 748 finalRuleCount++; 749 finalRuleIdx = i; 750 } 751 } 752 } 753 if (finalRuleCount > 2) { 754 // Too many final rules 755 return false; 756 } 757 758 if (finalRuleCount == 1) { 759 if (rules.size() == 1) { 760 // Only one final rule, only governs the initial rule, 761 // which is already initialized, thus, we do not need to 762 // add this transition rule 763 rules.clear(); 764 } else { 765 // Normalize the final rule 766 AnnualTimeZoneRule finalRule = (AnnualTimeZoneRule)rules.get(finalRuleIdx); 767 int tmpRaw = finalRule.getRawOffset(); 768 int tmpDST = finalRule.getDSTSavings(); 769 770 // Find the last non-final rule 771 Date finalStart = finalRule.getFirstStart(initialRawOffset, initialDSTSavings); 772 Date start = finalStart; 773 for (int i = 0; i < rules.size(); i++) { 774 if (finalRuleIdx == i) { 775 continue; 776 } 777 TimeZoneRule r = rules.get(i); 778 Date lastStart = r.getFinalStart(tmpRaw, tmpDST); 779 if (lastStart.after(start)) { 780 start = finalRule.getNextStart(lastStart.getTime(), 781 r.getRawOffset(), 782 r.getDSTSavings(), 783 false); 784 } 785 } 786 TimeZoneRule newRule; 787 if (start == finalStart) { 788 // Transform this into a single transition 789 newRule = new TimeArrayTimeZoneRule( 790 finalRule.getName(), 791 finalRule.getRawOffset(), 792 finalRule.getDSTSavings(), 793 new long[] {finalStart.getTime()}, 794 DateTimeRule.UTC_TIME); 795 } else { 796 // Update the end year 797 int fields[] = Grego.timeToFields(start.getTime(), null); 798 newRule = new AnnualTimeZoneRule( 799 finalRule.getName(), 800 finalRule.getRawOffset(), 801 finalRule.getDSTSavings(), 802 finalRule.getRule(), 803 finalRule.getStartYear(), 804 fields[0]); 805 } 806 rules.set(finalRuleIdx, newRule); 807 } 808 } 809 810 for (TimeZoneRule r : rules) { 811 rbtz.addTransitionRule(r); 812 } 813 814 tz = rbtz; 815 setID(tzid); 816 return true; 817 } 818 819 /* 820 * Create a default TZNAME from TZID 821 */ 822 private static String getDefaultTZName(String tzid, boolean isDST) { 823 if (isDST) { 824 return tzid + "(DST)"; 825 } 826 return tzid + "(STD)"; 827 } 828 829 /* 830 * Create a TimeZoneRule by the RRULE definition 831 */ 832 private static TimeZoneRule createRuleByRRULE(String tzname, 833 int rawOffset, int dstSavings, long start, List<String> dates, int fromOffset) { 834 if (dates == null || dates.size() == 0) { 835 return null; 836 } 837 // Parse the first rule 838 String rrule = dates.get(0); 839 840 long until[] = new long[1]; 841 int[] ruleFields = parseRRULE(rrule, until); 842 if (ruleFields == null) { 843 // Invalid RRULE 844 return null; 845 } 846 847 int month = ruleFields[0]; 848 int dayOfWeek = ruleFields[1]; 849 int nthDayOfWeek = ruleFields[2]; 850 int dayOfMonth = ruleFields[3]; 851 852 if (dates.size() == 1) { 853 // No more rules 854 if (ruleFields.length > 4) { 855 // Multiple BYMONTHDAY values 856 857 if (ruleFields.length != 10 || month == -1 || dayOfWeek == 0) { 858 // Only support the rule using 7 continuous days 859 // BYMONTH and BYDAY must be set at the same time 860 return null; 861 } 862 int firstDay = 31; // max possible number of dates in a month 863 int days[] = new int[7]; 864 for (int i = 0; i < 7; i++) { 865 days[i] = ruleFields[3 + i]; 866 // Resolve negative day numbers. A negative day number should 867 // not be used in February, but if we see such case, we use 28 868 // as the base. 869 days[i] = days[i] > 0 ? days[i] : MONTHLENGTH[month] + days[i] + 1; 870 firstDay = days[i] < firstDay ? days[i] : firstDay; 871 } 872 // Make sure days are continuous 873 for (int i = 1; i < 7; i++) { 874 boolean found = false; 875 for (int j = 0; j < 7; j++) { 876 if (days[j] == firstDay + i) { 877 found = true; 878 break; 879 } 880 } 881 if (!found) { 882 // days are not continuous 883 return null; 884 } 885 } 886 // Use DOW_GEQ_DOM rule with firstDay as the start date 887 dayOfMonth = firstDay; 888 } 889 } else { 890 // Check if BYMONTH + BYMONTHDAY + BYDAY rule with multiple RRULE lines. 891 // Otherwise, not supported. 892 if (month == -1 || dayOfWeek == 0 || dayOfMonth == 0) { 893 // This is not the case 894 return null; 895 } 896 // Parse the rest of rules if number of rules is not exceeding 7. 897 // We can only support 7 continuous days starting from a day of month. 898 if (dates.size() > 7) { 899 return null; 900 } 901 902 // Note: To check valid date range across multiple rule is a little 903 // bit complicated. For now, this code is not doing strict range 904 // checking across month boundary 905 906 int earliestMonth = month; 907 int daysCount = ruleFields.length - 3; 908 int earliestDay = 31; 909 for (int i = 0; i < daysCount; i++) { 910 int dom = ruleFields[3 + i]; 911 dom = dom > 0 ? dom : MONTHLENGTH[month] + dom + 1; 912 earliestDay = dom < earliestDay ? dom : earliestDay; 913 } 914 915 int anotherMonth = -1; 916 for (int i = 1; i < dates.size(); i++) { 917 rrule = dates.get(i); 918 long[] unt = new long[1]; 919 int[] fields = parseRRULE(rrule, unt); 920 921 // If UNTIL is newer than previous one, use the one 922 if (unt[0] > until[0]) { 923 until = unt; 924 } 925 926 // Check if BYMONTH + BYMONTHDAY + BYDAY rule 927 if (fields[0] == -1 || fields[1] == 0 || fields[3] == 0) { 928 return null; 929 } 930 // Count number of BYMONTHDAY 931 int count = fields.length - 3; 932 if (daysCount + count > 7) { 933 // We cannot support BYMONTHDAY more than 7 934 return null; 935 } 936 // Check if the same BYDAY is used. Otherwise, we cannot 937 // support the rule 938 if (fields[1] != dayOfWeek) { 939 return null; 940 } 941 // Check if the month is same or right next to the primary month 942 if (fields[0] != month) { 943 if (anotherMonth == -1) { 944 int diff = fields[0] - month; 945 if (diff == -11 || diff == -1) { 946 // Previous month 947 anotherMonth = fields[0]; 948 earliestMonth = anotherMonth; 949 // Reset earliest day 950 earliestDay = 31; 951 } else if (diff == 11 || diff == 1) { 952 // Next month 953 anotherMonth = fields[0]; 954 } else { 955 // The day range cannot exceed more than 2 months 956 return null; 957 } 958 } else if (fields[0] != month && fields[0] != anotherMonth) { 959 // The day range cannot exceed more than 2 months 960 return null; 961 } 962 } 963 // If ealier month, go through days to find the earliest day 964 if (fields[0] == earliestMonth) { 965 for (int j = 0; j < count; j++) { 966 int dom = fields[3 + j]; 967 dom = dom > 0 ? dom : MONTHLENGTH[fields[0]] + dom + 1; 968 earliestDay = dom < earliestDay ? dom : earliestDay; 969 } 970 } 971 daysCount += count; 972 } 973 if (daysCount != 7) { 974 // Number of BYMONTHDAY entries must be 7 975 return null; 976 } 977 month = earliestMonth; 978 dayOfMonth = earliestDay; 979 } 980 981 // Calculate start/end year and missing fields 982 int[] dfields = Grego.timeToFields(start + fromOffset, null); 983 int startYear = dfields[0]; 984 if (month == -1) { 985 // If MYMONTH is not set, use the month of DTSTART 986 month = dfields[1]; 987 } 988 if (dayOfWeek == 0 && nthDayOfWeek == 0 && dayOfMonth == 0) { 989 // If only YEARLY is set, use the day of DTSTART as BYMONTHDAY 990 dayOfMonth = dfields[2]; 991 } 992 int timeInDay = dfields[5]; 993 994 int endYear = AnnualTimeZoneRule.MAX_YEAR; 995 if (until[0] != MIN_TIME) { 996 Grego.timeToFields(until[0], dfields); 997 endYear = dfields[0]; 998 } 999 1000 // Create the AnnualDateTimeRule 1001 DateTimeRule adtr = null; 1002 if (dayOfWeek == 0 && nthDayOfWeek == 0 && dayOfMonth != 0) { 1003 // Day in month rule, for example, 15th day in the month 1004 adtr = new DateTimeRule(month, dayOfMonth, timeInDay, DateTimeRule.WALL_TIME); 1005 } else if (dayOfWeek != 0 && nthDayOfWeek != 0 && dayOfMonth == 0) { 1006 // Nth day of week rule, for example, last Sunday 1007 adtr = new DateTimeRule(month, nthDayOfWeek, dayOfWeek, timeInDay, DateTimeRule.WALL_TIME); 1008 } else if (dayOfWeek != 0 && nthDayOfWeek == 0 && dayOfMonth != 0) { 1009 // First day of week after day of month rule, for example, 1010 // first Sunday after 15th day in the month 1011 adtr = new DateTimeRule(month, dayOfMonth, dayOfWeek, true, timeInDay, DateTimeRule.WALL_TIME); 1012 } else { 1013 // RRULE attributes are insufficient 1014 return null; 1015 } 1016 1017 return new AnnualTimeZoneRule(tzname, rawOffset, dstSavings, adtr, startYear, endYear); 1018 } 1019 1020 /* 1021 * Parse individual RRULE 1022 * 1023 * On return - 1024 * 1025 * int[0] month calculated by BYMONTH - 1, or -1 when not found 1026 * int[1] day of week in BYDAY, or 0 when not found 1027 * int[2] day of week ordinal number in BYDAY, or 0 when not found 1028 * int[i >= 3] day of month, which could be multiple values, or 0 when not found 1029 * 1030 * or 1031 * 1032 * null on any error cases, for exmaple, FREQ=YEARLY is not available 1033 * 1034 * When UNTIL attribute is available, the time will be set to until[0], 1035 * otherwise, MIN_TIME 1036 */ 1037 private static int[] parseRRULE(String rrule, long[] until) { 1038 int month = -1; 1039 int dayOfWeek = 0; 1040 int nthDayOfWeek = 0; 1041 int[] dayOfMonth = null; 1042 1043 long untilTime = MIN_TIME; 1044 boolean yearly = false; 1045 boolean parseError = false; 1046 StringTokenizer st= new StringTokenizer(rrule, SEMICOLON); 1047 1048 while (st.hasMoreTokens()) { 1049 String attr, value; 1050 String prop = st.nextToken(); 1051 int sep = prop.indexOf(EQUALS_SIGN); 1052 if (sep != -1) { 1053 attr = prop.substring(0, sep); 1054 value = prop.substring(sep + 1); 1055 } else { 1056 parseError = true; 1057 break; 1058 } 1059 1060 if (attr.equals(ICAL_FREQ)) { 1061 // only support YEARLY frequency type 1062 if (value.equals(ICAL_YEARLY)) { 1063 yearly = true; 1064 } else { 1065 parseError = true; 1066 break; 1067 } 1068 } else if (attr.equals(ICAL_UNTIL)) { 1069 // ISO8601 UTC format, for example, "20060315T020000Z" 1070 try { 1071 untilTime = parseDateTimeString(value, 0); 1072 } catch (IllegalArgumentException iae) { 1073 parseError = true; 1074 break; 1075 } 1076 } else if (attr.equals(ICAL_BYMONTH)) { 1077 // Note: BYMONTH may contain multiple months, but only single month make sense for 1078 // VTIMEZONE property. 1079 if (value.length() > 2) { 1080 parseError = true; 1081 break; 1082 } 1083 try { 1084 month = Integer.parseInt(value) - 1; 1085 if (month < 0 || month >= 12) { 1086 parseError = true; 1087 break; 1088 } 1089 } catch (NumberFormatException nfe) { 1090 parseError = true; 1091 break; 1092 } 1093 } else if (attr.equals(ICAL_BYDAY)) { 1094 // Note: BYDAY may contain multiple day of week separated by comma. It is unlikely used for 1095 // VTIMEZONE property. We do not support the case. 1096 1097 // 2-letter format is used just for representing a day of week, for example, "SU" for Sunday 1098 // 3 or 4-letter format is used for represeinging Nth day of week, for example, "-1SA" for last Saturday 1099 int length = value.length(); 1100 if (length < 2 || length > 4) { 1101 parseError = true; 1102 break; 1103 } 1104 if (length > 2) { 1105 // Nth day of week 1106 int sign = 1; 1107 if (value.charAt(0) == '+') { 1108 sign = 1; 1109 } else if (value.charAt(0) == '-') { 1110 sign = -1; 1111 } else if (length == 4) { 1112 parseError = true; 1113 break; 1114 } 1115 try { 1116 int n = Integer.parseInt(value.substring(length - 3, length - 2)); 1117 if (n == 0 || n > 4) { 1118 parseError = true; 1119 break; 1120 } 1121 nthDayOfWeek = n * sign; 1122 } catch(NumberFormatException nfe) { 1123 parseError = true; 1124 break; 1125 } 1126 value = value.substring(length - 2); 1127 } 1128 int wday; 1129 for (wday = 0; wday < ICAL_DOW_NAMES.length; wday++) { 1130 if (value.equals(ICAL_DOW_NAMES[wday])) { 1131 break; 1132 } 1133 } 1134 if (wday < ICAL_DOW_NAMES.length) { 1135 // Sunday(1) - Saturday(7) 1136 dayOfWeek = wday + 1; 1137 } else { 1138 parseError = true; 1139 break; 1140 } 1141 } else if (attr.equals(ICAL_BYMONTHDAY)) { 1142 // Note: BYMONTHDAY may contain multiple days delimited by comma 1143 // 1144 // A value of BYMONTHDAY could be negative, for example, -1 means 1145 // the last day in a month 1146 StringTokenizer days = new StringTokenizer(value, COMMA); 1147 int count = days.countTokens(); 1148 dayOfMonth = new int[count]; 1149 int index = 0; 1150 while(days.hasMoreTokens()) { 1151 try { 1152 dayOfMonth[index++] = Integer.parseInt(days.nextToken()); 1153 } catch (NumberFormatException nfe) { 1154 parseError = true; 1155 break; 1156 } 1157 } 1158 } 1159 } 1160 1161 if (parseError) { 1162 return null; 1163 } 1164 if (!yearly) { 1165 // FREQ=YEARLY must be set 1166 return null; 1167 } 1168 1169 until[0] = untilTime; 1170 1171 int[] results; 1172 if (dayOfMonth == null) { 1173 results = new int[4]; 1174 results[3] = 0; 1175 } else { 1176 results = new int[3 + dayOfMonth.length]; 1177 for (int i = 0; i < dayOfMonth.length; i++) { 1178 results[3 + i] = dayOfMonth[i]; 1179 } 1180 } 1181 results[0] = month; 1182 results[1] = dayOfWeek; 1183 results[2] = nthDayOfWeek; 1184 return results; 1185 } 1186 1187 /* 1188 * Create a TimeZoneRule by the RDATE definition 1189 */ 1190 private static TimeZoneRule createRuleByRDATE(String tzname, 1191 int rawOffset, int dstSavings, long start, List<String> dates, int fromOffset) { 1192 // Create an array of transition times 1193 long[] times; 1194 if (dates == null || dates.size() == 0) { 1195 // When no RDATE line is provided, use start (DTSTART) 1196 // as the transition time 1197 times = new long[1]; 1198 times[0] = start; 1199 } else { 1200 times = new long[dates.size()]; 1201 int idx = 0; 1202 try { 1203 for (String date : dates) { 1204 times[idx++] = parseDateTimeString(date, fromOffset); 1205 } 1206 } catch (IllegalArgumentException iae) { 1207 return null; 1208 } 1209 } 1210 return new TimeArrayTimeZoneRule(tzname, rawOffset, dstSavings, times, DateTimeRule.UTC_TIME); 1211 } 1212 1213 /* 1214 * Write the time zone rules in RFC2445 VTIMEZONE format 1215 */ 1216 private void writeZone(Writer w, BasicTimeZone basictz, String[] customProperties) throws IOException { 1217 // Write the header 1218 writeHeader(w); 1219 1220 if (customProperties != null && customProperties.length > 0) { 1221 for (int i = 0; i < customProperties.length; i++) { 1222 if (customProperties[i] != null) { 1223 w.write(customProperties[i]); 1224 w.write(NEWLINE); 1225 } 1226 } 1227 } 1228 1229 long t = MIN_TIME; 1230 String dstName = null; 1231 int dstFromOffset = 0; 1232 int dstFromDSTSavings = 0; 1233 int dstToOffset = 0; 1234 int dstStartYear = 0; 1235 int dstMonth = 0; 1236 int dstDayOfWeek = 0; 1237 int dstWeekInMonth = 0; 1238 int dstMillisInDay = 0; 1239 long dstStartTime = 0; 1240 long dstUntilTime = 0; 1241 int dstCount = 0; 1242 AnnualTimeZoneRule finalDstRule = null; 1243 1244 String stdName = null; 1245 int stdFromOffset = 0; 1246 int stdFromDSTSavings = 0; 1247 int stdToOffset = 0; 1248 int stdStartYear = 0; 1249 int stdMonth = 0; 1250 int stdDayOfWeek = 0; 1251 int stdWeekInMonth = 0; 1252 int stdMillisInDay = 0; 1253 long stdStartTime = 0; 1254 long stdUntilTime = 0; 1255 int stdCount = 0; 1256 AnnualTimeZoneRule finalStdRule = null; 1257 1258 int[] dtfields = new int[6]; 1259 boolean hasTransitions = false; 1260 1261 // Going through all transitions 1262 while(true) { 1263 TimeZoneTransition tzt = basictz.getNextTransition(t, false); 1264 if (tzt == null) { 1265 break; 1266 } 1267 hasTransitions = true; 1268 t = tzt.getTime(); 1269 String name = tzt.getTo().getName(); 1270 boolean isDst = (tzt.getTo().getDSTSavings() != 0); 1271 int fromOffset = tzt.getFrom().getRawOffset() + tzt.getFrom().getDSTSavings(); 1272 int fromDSTSavings = tzt.getFrom().getDSTSavings(); 1273 int toOffset = tzt.getTo().getRawOffset() + tzt.getTo().getDSTSavings(); 1274 Grego.timeToFields(tzt.getTime() + fromOffset, dtfields); 1275 int weekInMonth = Grego.getDayOfWeekInMonth(dtfields[0], dtfields[1], dtfields[2]); 1276 int year = dtfields[0]; 1277 boolean sameRule = false; 1278 if (isDst) { 1279 if (finalDstRule == null && tzt.getTo() instanceof AnnualTimeZoneRule) { 1280 if (((AnnualTimeZoneRule)tzt.getTo()).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) { 1281 finalDstRule = (AnnualTimeZoneRule)tzt.getTo(); 1282 } 1283 } 1284 if (dstCount > 0) { 1285 if (year == dstStartYear + dstCount 1286 && name.equals(dstName) 1287 && dstFromOffset == fromOffset 1288 && dstToOffset == toOffset 1289 && dstMonth == dtfields[1] 1290 && dstDayOfWeek == dtfields[3] 1291 && dstWeekInMonth == weekInMonth 1292 && dstMillisInDay == dtfields[5]) { 1293 // Update until time 1294 dstUntilTime = t; 1295 dstCount++; 1296 sameRule = true; 1297 } 1298 if (!sameRule) { 1299 if (dstCount == 1) { 1300 writeZonePropsByTime(w, true, dstName, dstFromOffset, dstToOffset, 1301 dstStartTime, true); 1302 } else { 1303 writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset, 1304 dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, dstUntilTime); 1305 } 1306 } 1307 } 1308 if (!sameRule) { 1309 // Reset this DST information 1310 dstName = name; 1311 dstFromOffset = fromOffset; 1312 dstFromDSTSavings = fromDSTSavings; 1313 dstToOffset = toOffset; 1314 dstStartYear = year; 1315 dstMonth = dtfields[1]; 1316 dstDayOfWeek = dtfields[3]; 1317 dstWeekInMonth = weekInMonth; 1318 dstMillisInDay = dtfields[5]; 1319 dstStartTime = dstUntilTime = t; 1320 dstCount = 1; 1321 } 1322 if (finalStdRule != null && finalDstRule != null) { 1323 break; 1324 } 1325 } else { 1326 if (finalStdRule == null && tzt.getTo() instanceof AnnualTimeZoneRule) { 1327 if (((AnnualTimeZoneRule)tzt.getTo()).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) { 1328 finalStdRule = (AnnualTimeZoneRule)tzt.getTo(); 1329 } 1330 } 1331 if (stdCount > 0) { 1332 if (year == stdStartYear + stdCount 1333 && name.equals(stdName) 1334 && stdFromOffset == fromOffset 1335 && stdToOffset == toOffset 1336 && stdMonth == dtfields[1] 1337 && stdDayOfWeek == dtfields[3] 1338 && stdWeekInMonth == weekInMonth 1339 && stdMillisInDay == dtfields[5]) { 1340 // Update until time 1341 stdUntilTime = t; 1342 stdCount++; 1343 sameRule = true; 1344 } 1345 if (!sameRule) { 1346 if (stdCount == 1) { 1347 writeZonePropsByTime(w, false, stdName, stdFromOffset, stdToOffset, 1348 stdStartTime, true); 1349 } else { 1350 writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset, 1351 stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, stdUntilTime); 1352 } 1353 } 1354 } 1355 if (!sameRule) { 1356 // Reset this STD information 1357 stdName = name; 1358 stdFromOffset = fromOffset; 1359 stdFromDSTSavings = fromDSTSavings; 1360 stdToOffset = toOffset; 1361 stdStartYear = year; 1362 stdMonth = dtfields[1]; 1363 stdDayOfWeek = dtfields[3]; 1364 stdWeekInMonth = weekInMonth; 1365 stdMillisInDay = dtfields[5]; 1366 stdStartTime = stdUntilTime = t; 1367 stdCount = 1; 1368 } 1369 if (finalStdRule != null && finalDstRule != null) { 1370 break; 1371 } 1372 } 1373 } 1374 if (!hasTransitions) { 1375 // No transition - put a single non transition RDATE 1376 int offset = basictz.getOffset(0 /* any time */); 1377 boolean isDst = (offset != basictz.getRawOffset()); 1378 writeZonePropsByTime(w, isDst, getDefaultTZName(basictz.getID(), isDst), 1379 offset, offset, DEF_TZSTARTTIME - offset, false); 1380 } else { 1381 if (dstCount > 0) { 1382 if (finalDstRule == null) { 1383 if (dstCount == 1) { 1384 writeZonePropsByTime(w, true, dstName, dstFromOffset, dstToOffset, 1385 dstStartTime, true); 1386 } else { 1387 writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset, 1388 dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, dstUntilTime); 1389 } 1390 } else { 1391 if (dstCount == 1) { 1392 writeFinalRule(w, true, finalDstRule, 1393 dstFromOffset - dstFromDSTSavings, dstFromDSTSavings, dstStartTime); 1394 } else { 1395 // Use a single rule if possible 1396 if (isEquivalentDateRule(dstMonth, dstWeekInMonth, dstDayOfWeek, finalDstRule.getRule())) { 1397 writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset, 1398 dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, MAX_TIME); 1399 } else { 1400 // Not equivalent rule - write out two different rules 1401 writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset, 1402 dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, dstUntilTime); 1403 1404 Date nextStart = finalDstRule.getNextStart(dstUntilTime, 1405 dstFromOffset - dstFromDSTSavings, dstFromDSTSavings, false); 1406 1407 assert nextStart != null; 1408 if (nextStart != null) { 1409 writeFinalRule(w, true, finalDstRule, 1410 dstFromOffset - dstFromDSTSavings, dstFromDSTSavings, nextStart.getTime()); 1411 } 1412 } 1413 } 1414 } 1415 } 1416 if (stdCount > 0) { 1417 if (finalStdRule == null) { 1418 if (stdCount == 1) { 1419 writeZonePropsByTime(w, false, stdName, stdFromOffset, stdToOffset, 1420 stdStartTime, true); 1421 } else { 1422 writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset, 1423 stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, stdUntilTime); 1424 } 1425 } else { 1426 if (stdCount == 1) { 1427 writeFinalRule(w, false, finalStdRule, 1428 stdFromOffset - stdFromDSTSavings, stdFromDSTSavings, stdStartTime); 1429 } else { 1430 // Use a single rule if possible 1431 if (isEquivalentDateRule(stdMonth, stdWeekInMonth, stdDayOfWeek, finalStdRule.getRule())) { 1432 writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset, 1433 stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, MAX_TIME); 1434 } else { 1435 // Not equivalent rule - write out two different rules 1436 writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset, 1437 stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, stdUntilTime); 1438 1439 Date nextStart = finalStdRule.getNextStart(stdUntilTime, 1440 stdFromOffset - stdFromDSTSavings, stdFromDSTSavings, false); 1441 1442 assert nextStart != null; 1443 if (nextStart != null) { 1444 writeFinalRule(w, false, finalStdRule, 1445 stdFromOffset - stdFromDSTSavings, stdFromDSTSavings, nextStart.getTime()); 1446 1447 } 1448 } 1449 } 1450 } 1451 } 1452 } 1453 writeFooter(w); 1454 } 1455 1456 /* 1457 * Check if the DOW rule specified by month, weekInMonth and dayOfWeek is equivalent 1458 * to the DateTimerule. 1459 */ 1460 private static boolean isEquivalentDateRule(int month, int weekInMonth, int dayOfWeek, DateTimeRule dtrule) { 1461 if (month != dtrule.getRuleMonth() || dayOfWeek != dtrule.getRuleDayOfWeek()) { 1462 return false; 1463 } 1464 if (dtrule.getTimeRuleType() != DateTimeRule.WALL_TIME) { 1465 // Do not try to do more intelligent comparison for now. 1466 return false; 1467 } 1468 if (dtrule.getDateRuleType() == DateTimeRule.DOW 1469 && dtrule.getRuleWeekInMonth() == weekInMonth) { 1470 return true; 1471 } 1472 int ruleDOM = dtrule.getRuleDayOfMonth(); 1473 if (dtrule.getDateRuleType() == DateTimeRule.DOW_GEQ_DOM) { 1474 if (ruleDOM%7 == 1 && (ruleDOM + 6)/7 == weekInMonth) { 1475 return true; 1476 } 1477 if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - ruleDOM)%7 == 6 1478 && weekInMonth == -1*((MONTHLENGTH[month]-ruleDOM+1)/7)) { 1479 return true; 1480 } 1481 } 1482 if (dtrule.getDateRuleType() == DateTimeRule.DOW_LEQ_DOM) { 1483 if (ruleDOM%7 == 0 && ruleDOM/7 == weekInMonth) { 1484 return true; 1485 } 1486 if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - ruleDOM)%7 == 0 1487 && weekInMonth == -1*((MONTHLENGTH[month] - ruleDOM)/7 + 1)) { 1488 return true; 1489 } 1490 } 1491 return false; 1492 } 1493 1494 /* 1495 * Write a single start time 1496 */ 1497 private static void writeZonePropsByTime(Writer writer, boolean isDst, String tzname, 1498 int fromOffset, int toOffset, long time, boolean withRDATE) throws IOException { 1499 beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, time); 1500 if (withRDATE) { 1501 writer.write(ICAL_RDATE); 1502 writer.write(COLON); 1503 writer.write(getDateTimeString(time + fromOffset)); 1504 writer.write(NEWLINE); 1505 } 1506 endZoneProps(writer, isDst); 1507 } 1508 1509 /* 1510 * Write start times defined by a DOM rule using VTIMEZONE RRULE 1511 */ 1512 private static void writeZonePropsByDOM(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset, 1513 int month, int dayOfMonth, long startTime, long untilTime) throws IOException { 1514 beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, startTime); 1515 1516 beginRRULE(writer, month); 1517 writer.write(ICAL_BYMONTHDAY); 1518 writer.write(EQUALS_SIGN); 1519 writer.write(Integer.toString(dayOfMonth)); 1520 1521 if (untilTime != MAX_TIME) { 1522 appendUNTIL(writer, getDateTimeString(untilTime + fromOffset)); 1523 } 1524 writer.write(NEWLINE); 1525 1526 endZoneProps(writer, isDst); 1527 } 1528 1529 /* 1530 * Write start times defined by a DOW rule using VTIMEZONE RRULE 1531 */ 1532 private static void writeZonePropsByDOW(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset, 1533 int month, int weekInMonth, int dayOfWeek, long startTime, long untilTime) throws IOException { 1534 beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, startTime); 1535 1536 beginRRULE(writer, month); 1537 writer.write(ICAL_BYDAY); 1538 writer.write(EQUALS_SIGN); 1539 writer.write(Integer.toString(weekInMonth)); // -4, -3, -2, -1, 1, 2, 3, 4 1540 writer.write(ICAL_DOW_NAMES[dayOfWeek - 1]); // SU, MO, TU... 1541 1542 if (untilTime != MAX_TIME) { 1543 appendUNTIL(writer, getDateTimeString(untilTime + fromOffset)); 1544 } 1545 writer.write(NEWLINE); 1546 1547 endZoneProps(writer, isDst); 1548 } 1549 1550 /* 1551 * Write start times defined by a DOW_GEQ_DOM rule using VTIMEZONE RRULE 1552 */ 1553 private static void writeZonePropsByDOW_GEQ_DOM(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset, 1554 int month, int dayOfMonth, int dayOfWeek, long startTime, long untilTime) throws IOException { 1555 // Check if this rule can be converted to DOW rule 1556 if (dayOfMonth%7 == 1) { 1557 // Can be represented by DOW rule 1558 writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset, 1559 month, (dayOfMonth + 6)/7, dayOfWeek, startTime, untilTime); 1560 } else if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - dayOfMonth)%7 == 6) { 1561 // Can be represented by DOW rule with negative week number 1562 writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset, 1563 month, -1*((MONTHLENGTH[month] - dayOfMonth + 1)/7), dayOfWeek, startTime, untilTime); 1564 } else { 1565 // Otherwise, use BYMONTHDAY to include all possible dates 1566 beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, startTime); 1567 1568 // Check if all days are in the same month 1569 int startDay = dayOfMonth; 1570 int currentMonthDays = 7; 1571 1572 if (dayOfMonth <= 0) { 1573 // The start day is in previous month 1574 int prevMonthDays = 1 - dayOfMonth; 1575 currentMonthDays -= prevMonthDays; 1576 1577 int prevMonth = (month - 1) < 0 ? 11 : month - 1; 1578 1579 // Note: When a rule is separated into two, UNTIL attribute needs to be 1580 // calculated for each of them. For now, we skip this, because we basically use this method 1581 // only for final rules, which does not have the UNTIL attribute 1582 writeZonePropsByDOW_GEQ_DOM_sub(writer, prevMonth, -prevMonthDays, dayOfWeek, prevMonthDays, MAX_TIME /* Do not use UNTIL */, fromOffset); 1583 1584 // Start from 1 for the rest 1585 startDay = 1; 1586 } else if (dayOfMonth + 6 > MONTHLENGTH[month]) { 1587 // Note: This code does not actually work well in February. For now, days in month in 1588 // non-leap year. 1589 int nextMonthDays = dayOfMonth + 6 - MONTHLENGTH[month]; 1590 currentMonthDays -= nextMonthDays; 1591 1592 int nextMonth = (month + 1) > 11 ? 0 : month + 1; 1593 1594 writeZonePropsByDOW_GEQ_DOM_sub(writer, nextMonth, 1, dayOfWeek, nextMonthDays, MAX_TIME /* Do not use UNTIL */, fromOffset); 1595 } 1596 writeZonePropsByDOW_GEQ_DOM_sub(writer, month, startDay, dayOfWeek, currentMonthDays, untilTime, fromOffset); 1597 endZoneProps(writer, isDst); 1598 } 1599 } 1600 1601 /* 1602 * Called from writeZonePropsByDOW_GEQ_DOM 1603 */ 1604 private static void writeZonePropsByDOW_GEQ_DOM_sub(Writer writer, int month, 1605 int dayOfMonth, int dayOfWeek, int numDays, long untilTime, int fromOffset) throws IOException { 1606 1607 int startDayNum = dayOfMonth; 1608 boolean isFeb = (month == Calendar.FEBRUARY); 1609 if (dayOfMonth < 0 && !isFeb) { 1610 // Use positive number if possible 1611 startDayNum = MONTHLENGTH[month] + dayOfMonth + 1; 1612 } 1613 beginRRULE(writer, month); 1614 writer.write(ICAL_BYDAY); 1615 writer.write(EQUALS_SIGN); 1616 writer.write(ICAL_DOW_NAMES[dayOfWeek - 1]); // SU, MO, TU... 1617 writer.write(SEMICOLON); 1618 writer.write(ICAL_BYMONTHDAY); 1619 writer.write(EQUALS_SIGN); 1620 1621 writer.write(Integer.toString(startDayNum)); 1622 for (int i = 1; i < numDays; i++) { 1623 writer.write(COMMA); 1624 writer.write(Integer.toString(startDayNum + i)); 1625 } 1626 1627 if (untilTime != MAX_TIME) { 1628 appendUNTIL(writer, getDateTimeString(untilTime + fromOffset)); 1629 } 1630 writer.write(NEWLINE); 1631 } 1632 1633 /* 1634 * Write start times defined by a DOW_LEQ_DOM rule using VTIMEZONE RRULE 1635 */ 1636 private static void writeZonePropsByDOW_LEQ_DOM(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset, 1637 int month, int dayOfMonth, int dayOfWeek, long startTime, long untilTime) throws IOException { 1638 // Check if this rule can be converted to DOW rule 1639 if (dayOfMonth%7 == 0) { 1640 // Can be represented by DOW rule 1641 writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset, 1642 month, dayOfMonth/7, dayOfWeek, startTime, untilTime); 1643 } else if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - dayOfMonth)%7 == 0){ 1644 // Can be represented by DOW rule with negative week number 1645 writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset, 1646 month, -1*((MONTHLENGTH[month] - dayOfMonth)/7 + 1), dayOfWeek, startTime, untilTime); 1647 } else if (month == Calendar.FEBRUARY && dayOfMonth == 29) { 1648 // Specical case for February 1649 writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset, 1650 Calendar.FEBRUARY, -1, dayOfWeek, startTime, untilTime); 1651 } else { 1652 // Otherwise, convert this to DOW_GEQ_DOM rule 1653 writeZonePropsByDOW_GEQ_DOM(writer, isDst, tzname, fromOffset, toOffset, 1654 month, dayOfMonth - 6, dayOfWeek, startTime, untilTime); 1655 } 1656 } 1657 1658 /* 1659 * Write the final time zone rule using RRULE, with no UNTIL attribute 1660 */ 1661 private static void writeFinalRule(Writer writer, boolean isDst, AnnualTimeZoneRule rule, 1662 int fromRawOffset, int fromDSTSavings, long startTime) throws IOException{ 1663 DateTimeRule dtrule = toWallTimeRule(rule.getRule(), fromRawOffset, fromDSTSavings); 1664 1665 // If the rule's mills in a day is out of range, adjust start time. 1666 // Olson tzdata supports 24:00 of a day, but VTIMEZONE does not. 1667 // See ticket#7008/#7518 1668 1669 int timeInDay = dtrule.getRuleMillisInDay(); 1670 if (timeInDay < 0) { 1671 startTime = startTime + (0 - timeInDay); 1672 } else if (timeInDay >= Grego.MILLIS_PER_DAY) { 1673 startTime = startTime - (timeInDay - (Grego.MILLIS_PER_DAY - 1)); 1674 } 1675 1676 int toOffset = rule.getRawOffset() + rule.getDSTSavings(); 1677 switch (dtrule.getDateRuleType()) { 1678 case DateTimeRule.DOM: 1679 writeZonePropsByDOM(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset, 1680 dtrule.getRuleMonth(), dtrule.getRuleDayOfMonth(), startTime, MAX_TIME); 1681 break; 1682 case DateTimeRule.DOW: 1683 writeZonePropsByDOW(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset, 1684 dtrule.getRuleMonth(), dtrule.getRuleWeekInMonth(), dtrule.getRuleDayOfWeek(), startTime, MAX_TIME); 1685 break; 1686 case DateTimeRule.DOW_GEQ_DOM: 1687 writeZonePropsByDOW_GEQ_DOM(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset, 1688 dtrule.getRuleMonth(), dtrule.getRuleDayOfMonth(), dtrule.getRuleDayOfWeek(), startTime, MAX_TIME); 1689 break; 1690 case DateTimeRule.DOW_LEQ_DOM: 1691 writeZonePropsByDOW_LEQ_DOM(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset, 1692 dtrule.getRuleMonth(), dtrule.getRuleDayOfMonth(), dtrule.getRuleDayOfWeek(), startTime, MAX_TIME); 1693 break; 1694 } 1695 } 1696 1697 /* 1698 * Convert the rule to its equivalent rule using WALL_TIME mode 1699 */ 1700 private static DateTimeRule toWallTimeRule(DateTimeRule rule, int rawOffset, int dstSavings) { 1701 if (rule.getTimeRuleType() == DateTimeRule.WALL_TIME) { 1702 return rule; 1703 } 1704 int wallt = rule.getRuleMillisInDay(); 1705 if (rule.getTimeRuleType() == DateTimeRule.UTC_TIME) { 1706 wallt += (rawOffset + dstSavings); 1707 } else if (rule.getTimeRuleType() == DateTimeRule.STANDARD_TIME) { 1708 wallt += dstSavings; 1709 } 1710 1711 int month = -1, dom = 0, dow = 0, dtype = -1; 1712 int dshift = 0; 1713 if (wallt < 0) { 1714 dshift = -1; 1715 wallt += Grego.MILLIS_PER_DAY; 1716 } else if (wallt >= Grego.MILLIS_PER_DAY) { 1717 dshift = 1; 1718 wallt -= Grego.MILLIS_PER_DAY; 1719 } 1720 1721 month = rule.getRuleMonth(); 1722 dom = rule.getRuleDayOfMonth(); 1723 dow = rule.getRuleDayOfWeek(); 1724 dtype = rule.getDateRuleType(); 1725 1726 if (dshift != 0) { 1727 if (dtype == DateTimeRule.DOW) { 1728 // Convert to DOW_GEW_DOM or DOW_LEQ_DOM rule first 1729 int wim = rule.getRuleWeekInMonth(); 1730 if (wim > 0) { 1731 dtype = DateTimeRule.DOW_GEQ_DOM; 1732 dom = 7 * (wim - 1) + 1; 1733 } else { 1734 dtype = DateTimeRule.DOW_LEQ_DOM; 1735 dom = MONTHLENGTH[month] + 7 * (wim + 1); 1736 } 1737 1738 } 1739 // Shift one day before or after 1740 dom += dshift; 1741 if (dom == 0) { 1742 month--; 1743 month = month < Calendar.JANUARY ? Calendar.DECEMBER : month; 1744 dom = MONTHLENGTH[month]; 1745 } else if (dom > MONTHLENGTH[month]) { 1746 month++; 1747 month = month > Calendar.DECEMBER ? Calendar.JANUARY : month; 1748 dom = 1; 1749 } 1750 if (dtype != DateTimeRule.DOM) { 1751 // Adjust day of week 1752 dow += dshift; 1753 if (dow < Calendar.SUNDAY) { 1754 dow = Calendar.SATURDAY; 1755 } else if (dow > Calendar.SATURDAY) { 1756 dow = Calendar.SUNDAY; 1757 } 1758 } 1759 } 1760 // Create a new rule 1761 DateTimeRule modifiedRule; 1762 if (dtype == DateTimeRule.DOM) { 1763 modifiedRule = new DateTimeRule(month, dom, wallt, DateTimeRule.WALL_TIME); 1764 } else { 1765 modifiedRule = new DateTimeRule(month, dom, dow, 1766 (dtype == DateTimeRule.DOW_GEQ_DOM), wallt, DateTimeRule.WALL_TIME); 1767 } 1768 return modifiedRule; 1769 } 1770 1771 /* 1772 * Write the opening section of zone properties 1773 */ 1774 private static void beginZoneProps(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset, long startTime) throws IOException { 1775 writer.write(ICAL_BEGIN); 1776 writer.write(COLON); 1777 if (isDst) { 1778 writer.write(ICAL_DAYLIGHT); 1779 } else { 1780 writer.write(ICAL_STANDARD); 1781 } 1782 writer.write(NEWLINE); 1783 1784 // TZOFFSETTO 1785 writer.write(ICAL_TZOFFSETTO); 1786 writer.write(COLON); 1787 writer.write(millisToOffset(toOffset)); 1788 writer.write(NEWLINE); 1789 1790 // TZOFFSETFROM 1791 writer.write(ICAL_TZOFFSETFROM); 1792 writer.write(COLON); 1793 writer.write(millisToOffset(fromOffset)); 1794 writer.write(NEWLINE); 1795 1796 // TZNAME 1797 writer.write(ICAL_TZNAME); 1798 writer.write(COLON); 1799 writer.write(tzname); 1800 writer.write(NEWLINE); 1801 1802 // DTSTART 1803 writer.write(ICAL_DTSTART); 1804 writer.write(COLON); 1805 writer.write(getDateTimeString(startTime + fromOffset)); 1806 writer.write(NEWLINE); 1807 } 1808 1809 /* 1810 * Writes the closing section of zone properties 1811 */ 1812 private static void endZoneProps(Writer writer, boolean isDst) throws IOException{ 1813 // END:STANDARD or END:DAYLIGHT 1814 writer.write(ICAL_END); 1815 writer.write(COLON); 1816 if (isDst) { 1817 writer.write(ICAL_DAYLIGHT); 1818 } else { 1819 writer.write(ICAL_STANDARD); 1820 } 1821 writer.write(NEWLINE); 1822 } 1823 1824 /* 1825 * Write the beginning part of RRULE line 1826 */ 1827 private static void beginRRULE(Writer writer, int month) throws IOException { 1828 writer.write(ICAL_RRULE); 1829 writer.write(COLON); 1830 writer.write(ICAL_FREQ); 1831 writer.write(EQUALS_SIGN); 1832 writer.write(ICAL_YEARLY); 1833 writer.write(SEMICOLON); 1834 writer.write(ICAL_BYMONTH); 1835 writer.write(EQUALS_SIGN); 1836 writer.write(Integer.toString(month + 1)); 1837 writer.write(SEMICOLON); 1838 } 1839 1840 /* 1841 * Append the UNTIL attribute after RRULE line 1842 */ 1843 private static void appendUNTIL(Writer writer, String until) throws IOException { 1844 if (until != null) { 1845 writer.write(SEMICOLON); 1846 writer.write(ICAL_UNTIL); 1847 writer.write(EQUALS_SIGN); 1848 writer.write(until); 1849 } 1850 } 1851 1852 /* 1853 * Write the opening section of the VTIMEZONE block 1854 */ 1855 private void writeHeader(Writer writer)throws IOException { 1856 writer.write(ICAL_BEGIN); 1857 writer.write(COLON); 1858 writer.write(ICAL_VTIMEZONE); 1859 writer.write(NEWLINE); 1860 writer.write(ICAL_TZID); 1861 writer.write(COLON); 1862 writer.write(tz.getID()); 1863 writer.write(NEWLINE); 1864 if (tzurl != null) { 1865 writer.write(ICAL_TZURL); 1866 writer.write(COLON); 1867 writer.write(tzurl); 1868 writer.write(NEWLINE); 1869 } 1870 if (lastmod != null) { 1871 writer.write(ICAL_LASTMOD); 1872 writer.write(COLON); 1873 writer.write(getUTCDateTimeString(lastmod.getTime())); 1874 writer.write(NEWLINE); 1875 } 1876 } 1877 1878 /* 1879 * Write the closing section of the VTIMEZONE definition block 1880 */ 1881 private static void writeFooter(Writer writer) throws IOException { 1882 writer.write(ICAL_END); 1883 writer.write(COLON); 1884 writer.write(ICAL_VTIMEZONE); 1885 writer.write(NEWLINE); 1886 } 1887 1888 /* 1889 * Convert date/time to RFC2445 Date-Time form #1 DATE WITH LOCAL TIME 1890 */ 1891 private static String getDateTimeString(long time) { 1892 int[] fields = Grego.timeToFields(time, null); 1893 StringBuilder sb = new StringBuilder(15); 1894 sb.append(numToString(fields[0], 4)); 1895 sb.append(numToString(fields[1] + 1, 2)); 1896 sb.append(numToString(fields[2], 2)); 1897 sb.append('T'); 1898 1899 int t = fields[5]; 1900 int hour = t / Grego.MILLIS_PER_HOUR; 1901 t %= Grego.MILLIS_PER_HOUR; 1902 int min = t / Grego.MILLIS_PER_MINUTE; 1903 t %= Grego.MILLIS_PER_MINUTE; 1904 int sec = t / Grego.MILLIS_PER_SECOND; 1905 1906 sb.append(numToString(hour, 2)); 1907 sb.append(numToString(min, 2)); 1908 sb.append(numToString(sec, 2)); 1909 return sb.toString(); 1910 } 1911 1912 /* 1913 * Convert date/time to RFC2445 Date-Time form #2 DATE WITH UTC TIME 1914 */ 1915 private static String getUTCDateTimeString(long time) { 1916 return getDateTimeString(time) + "Z"; 1917 } 1918 1919 /* 1920 * Parse RFC2445 Date-Time form #1 DATE WITH LOCAL TIME and 1921 * #2 DATE WITH UTC TIME 1922 */ 1923 private static long parseDateTimeString(String str, int offset) { 1924 int year = 0, month = 0, day = 0, hour = 0, min = 0, sec = 0; 1925 boolean isUTC = false; 1926 boolean isValid = false; 1927 do { 1928 if (str == null) { 1929 break; 1930 } 1931 1932 int length = str.length(); 1933 if (length != 15 && length != 16) { 1934 // FORM#1 15 characters, such as "20060317T142115" 1935 // FORM#2 16 characters, such as "20060317T142115Z" 1936 break; 1937 } 1938 if (str.charAt(8) != 'T') { 1939 // charcter "T" must be used for separating date and time 1940 break; 1941 } 1942 if (length == 16) { 1943 if (str.charAt(15) != 'Z') { 1944 // invalid format 1945 break; 1946 } 1947 isUTC = true; 1948 } 1949 1950 try { 1951 year = Integer.parseInt(str.substring(0, 4)); 1952 month = Integer.parseInt(str.substring(4, 6)) - 1; // 0-based 1953 day = Integer.parseInt(str.substring(6, 8)); 1954 hour = Integer.parseInt(str.substring(9, 11)); 1955 min = Integer.parseInt(str.substring(11, 13)); 1956 sec = Integer.parseInt(str.substring(13, 15)); 1957 } catch (NumberFormatException nfe) { 1958 break; 1959 } 1960 1961 // check valid range 1962 int maxDayOfMonth = Grego.monthLength(year, month); 1963 if (year < 0 || month < 0 || month > 11 || day < 1 || day > maxDayOfMonth || 1964 hour < 0 || hour >= 24 || min < 0 || min >= 60 || sec < 0 || sec >= 60) { 1965 break; 1966 } 1967 1968 isValid = true; 1969 } while(false); 1970 1971 if (!isValid) { 1972 throw new IllegalArgumentException("Invalid date time string format"); 1973 } 1974 // Calculate the time 1975 long time = Grego.fieldsToDay(year, month, day) * Grego.MILLIS_PER_DAY; 1976 time += (hour*Grego.MILLIS_PER_HOUR + min*Grego.MILLIS_PER_MINUTE + sec*Grego.MILLIS_PER_SECOND); 1977 if (!isUTC) { 1978 time -= offset; 1979 } 1980 return time; 1981 } 1982 1983 /* 1984 * Convert RFC2445 utc-offset string to milliseconds 1985 */ 1986 private static int offsetStrToMillis(String str) { 1987 boolean isValid = false; 1988 int sign = 0, hour = 0, min = 0, sec = 0; 1989 1990 do { 1991 if (str == null) { 1992 break; 1993 } 1994 int length = str.length(); 1995 if (length != 5 && length != 7) { 1996 // utf-offset must be 5 or 7 characters 1997 break; 1998 } 1999 // sign 2000 char s = str.charAt(0); 2001 if (s == '+') { 2002 sign = 1; 2003 } else if (s == '-') { 2004 sign = -1; 2005 } else { 2006 // utf-offset must start with "+" or "-" 2007 break; 2008 } 2009 2010 try { 2011 hour = Integer.parseInt(str.substring(1, 3)); 2012 min = Integer.parseInt(str.substring(3, 5)); 2013 if (length == 7) { 2014 sec = Integer.parseInt(str.substring(5, 7)); 2015 } 2016 } catch (NumberFormatException nfe) { 2017 break; 2018 } 2019 isValid = true; 2020 } while(false); 2021 2022 if (!isValid) { 2023 throw new IllegalArgumentException("Bad offset string"); 2024 } 2025 int millis = sign * ((hour * 60 + min) * 60 + sec) * 1000; 2026 return millis; 2027 } 2028 2029 /* 2030 * Convert milliseconds to RFC2445 utc-offset string 2031 */ 2032 private static String millisToOffset(int millis) { 2033 StringBuilder sb = new StringBuilder(7); 2034 if (millis >= 0) { 2035 sb.append('+'); 2036 } else { 2037 sb.append('-'); 2038 millis = -millis; 2039 } 2040 int hour, min, sec; 2041 int t = millis / 1000; 2042 2043 sec = t % 60; 2044 t = (t - sec) / 60; 2045 min = t % 60; 2046 hour = t / 60; 2047 2048 sb.append(numToString(hour, 2)); 2049 sb.append(numToString(min, 2)); 2050 sb.append(numToString(sec, 2)); 2051 2052 return sb.toString(); 2053 } 2054 2055 /* 2056 * Format integer number 2057 */ 2058 private static String numToString(int num, int width) { 2059 String str = Integer.toString(num); 2060 int len = str.length(); 2061 if (len >= width) { 2062 return str.substring(len - width, len); 2063 } 2064 StringBuilder sb = new StringBuilder(width); 2065 for (int i = len; i < width; i++) { 2066 sb.append('0'); 2067 } 2068 sb.append(str); 2069 return sb.toString(); 2070 } 2071 2072 // Freezable stuffs 2073 private volatile transient boolean isFrozen = false; 2074 2075 /** 2076 * {@inheritDoc} 2077 */ 2078 public boolean isFrozen() { 2079 return isFrozen; 2080 } 2081 2082 /** 2083 * {@inheritDoc} 2084 */ 2085 public TimeZone freeze() { 2086 isFrozen = true; 2087 return this; 2088 } 2089 2090 /** 2091 * {@inheritDoc} 2092 */ 2093 public TimeZone cloneAsThawed() { 2094 VTimeZone vtz = (VTimeZone)super.cloneAsThawed(); 2095 vtz.tz = (BasicTimeZone)tz.cloneAsThawed(); 2096 vtz.isFrozen = false; 2097 return vtz; 2098 } 2099} 2100