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