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