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