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