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