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 */ 10 package ohos.global.icu.util; 11 12 import java.io.BufferedWriter; 13 import java.io.IOException; 14 import java.io.Reader; 15 import java.io.Writer; 16 import java.util.ArrayList; 17 import java.util.Date; 18 import java.util.LinkedList; 19 import java.util.List; 20 import java.util.MissingResourceException; 21 import java.util.StringTokenizer; 22 23 import ohos.global.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 exposed on OHOS 38 */ 39 public 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 */ create(String tzid)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 */ create(Reader reader)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 getOffset(int era, int year, int month, int day, int dayOfWeek, int milliseconds)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 getOffset(long date, boolean local, int[] offsets)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 OHOS 98 */ 99 @Deprecated 100 @Override getOffsetFromLocal(long date, int nonExistingTimeOpt, int duplicatedTimeOpt, int[] offsets)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 getRawOffset()110 public int getRawOffset() { 111 return tz.getRawOffset(); 112 } 113 114 /** 115 * {@inheritDoc} 116 */ 117 @Override inDaylightTime(Date date)118 public boolean inDaylightTime(Date date) { 119 return tz.inDaylightTime(date); 120 } 121 122 /** 123 * {@inheritDoc} 124 */ 125 @Override setRawOffset(int offsetMillis)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 useDaylightTime()137 public boolean useDaylightTime() { 138 return tz.useDaylightTime(); 139 } 140 141 /** 142 * {@inheritDoc} 143 */ 144 @Override observesDaylightTime()145 public boolean observesDaylightTime() { 146 return tz.observesDaylightTime(); 147 } 148 149 /** 150 * {@inheritDoc} 151 */ 152 @Override hasSameRules(TimeZone other)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 */ getTZURL()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 */ setTZURL(String url)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 */ getLastModified()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 */ setLastModified(Date date)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 */ write(Writer writer)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 */ write(Writer writer, long start)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 */ writeSimple(Writer writer, long time)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 getNextTransition(long base, boolean inclusive)314 public TimeZoneTransition getNextTransition(long base, boolean inclusive) { 315 return tz.getNextTransition(base, inclusive); 316 } 317 318 /** 319 * {@inheritDoc} 320 */ 321 @Override getPreviousTransition(long base, boolean inclusive)322 public TimeZoneTransition getPreviousTransition(long base, boolean inclusive) { 323 return tz.getPreviousTransition(base, inclusive); 324 } 325 326 /** 327 * {@inheritDoc} 328 */ 329 @Override hasEquivalentTransitions(TimeZone other, long start, long end)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 getTimeZoneRules()341 public TimeZoneRule[] getTimeZoneRules() { 342 return tz.getTimeZoneRules(); 343 } 344 345 /** 346 * {@inheritDoc} 347 */ 348 @Override getTimeZoneRules(long start)349 public TimeZoneRule[] getTimeZoneRules(long start) { 350 return tz.getTimeZoneRules(start); 351 } 352 353 /** 354 * {@inheritDoc} 355 */ 356 @Override clone()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 */ VTimeZone()435 private VTimeZone() { 436 } 437 VTimeZone(String tzid)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 */ load(Reader reader)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 */ parse()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 */ getDefaultTZName(String tzid, boolean isDST)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 */ createRuleByRRULE(String tzname, int rawOffset, int dstSavings, long start, List<String> dates, int fromOffset)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 */ createRuleByRDATE(String tzname, int rawOffset, int dstSavings, long start, List<String> dates, int fromOffset)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 */ writeZone(Writer w, BasicTimeZone basictz, String[] customProperties)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 */ isEquivalentDateRule(int month, int weekInMonth, int dayOfWeek, DateTimeRule dtrule)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 */ writeZonePropsByTime(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset, long time, boolean withRDATE)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 */ writeZonePropsByDOM(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset, int month, int dayOfMonth, long startTime, long untilTime)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 */ writeZonePropsByDOW(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset, int month, int weekInMonth, int dayOfWeek, long startTime, long untilTime)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 */ writeZonePropsByDOW_GEQ_DOM(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset, int month, int dayOfMonth, int dayOfWeek, long startTime, long untilTime)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 */ writeZonePropsByDOW_GEQ_DOM_sub(Writer writer, int month, int dayOfMonth, int dayOfWeek, int numDays, long untilTime, int fromOffset)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 */ writeZonePropsByDOW_LEQ_DOM(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset, int month, int dayOfMonth, int dayOfWeek, long startTime, long untilTime)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 */ writeFinalRule(Writer writer, boolean isDst, AnnualTimeZoneRule rule, int fromRawOffset, int fromDSTSavings, long startTime)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 */ toWallTimeRule(DateTimeRule rule, int rawOffset, int dstSavings)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 */ beginZoneProps(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset, long startTime)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 */ endZoneProps(Writer writer, boolean isDst)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 */ beginRRULE(Writer writer, int month)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 */ appendUNTIL(Writer writer, String until)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 */ writeHeader(Writer writer)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 */ writeFooter(Writer writer)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 */ getDateTimeString(long time)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 */ getUTCDateTimeString(long time)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 */ parseDateTimeString(String str, int offset)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 */ offsetStrToMillis(String str)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 */ millisToOffset(int millis)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 */ numToString(int num, int width)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 */ isFrozen()2078 public boolean isFrozen() { 2079 return isFrozen; 2080 } 2081 2082 /** 2083 * {@inheritDoc} 2084 */ freeze()2085 public TimeZone freeze() { 2086 isFrozen = true; 2087 return this; 2088 } 2089 2090 /** 2091 * {@inheritDoc} 2092 */ cloneAsThawed()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