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