View Javadoc

1   /*
2    *  Copyright 2001-2013 Stephen Colebourne
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   */
16  package org.joda.time.tz;
17  
18  import java.io.BufferedReader;
19  import java.io.DataOutputStream;
20  import java.io.File;
21  import java.io.FileInputStream;
22  import java.io.FileOutputStream;
23  import java.io.FileReader;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.OutputStream;
27  import java.util.ArrayList;
28  import java.util.HashMap;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.Map;
32  import java.util.StringTokenizer;
33  import java.util.TreeMap;
34  import java.util.Map.Entry;
35  
36  import org.joda.time.Chronology;
37  import org.joda.time.DateTime;
38  import org.joda.time.DateTimeField;
39  import org.joda.time.DateTimeZone;
40  import org.joda.time.LocalDate;
41  import org.joda.time.MutableDateTime;
42  import org.joda.time.chrono.ISOChronology;
43  import org.joda.time.chrono.LenientChronology;
44  import org.joda.time.format.DateTimeFormatter;
45  import org.joda.time.format.ISODateTimeFormat;
46  
47  /**
48   * Compiles Olson ZoneInfo database files into binary files for each time zone
49   * in the database. {@link DateTimeZoneBuilder} is used to construct and encode
50   * compiled data files. {@link ZoneInfoProvider} loads the encoded files and
51   * converts them back into {@link DateTimeZone} objects.
52   * <p>
53   * Although this tool is similar to zic, the binary formats are not
54   * compatible. The latest Olson database files may be obtained
55   * <a href="http://www.twinsun.com/tz/tz-link.htm">here</a>.
56   * <p>
57   * ZoneInfoCompiler is mutable and not thread-safe, although the main method
58   * may be safely invoked by multiple threads.
59   *
60   * @author Brian S O'Neill
61   * @since 1.0
62   */
63  public class ZoneInfoCompiler {
64      static DateTimeOfYear cStartOfYear;
65  
66      static Chronology cLenientISO;
67  
68      static ThreadLocal<Boolean> cVerbose = new ThreadLocal<Boolean>();
69      static {
70          cVerbose.set(Boolean.FALSE);
71      }
72  
73      /**
74       * Gets a flag indicating that verbose logging is required.
75       * @return true to log verbosely
76       */
77      public static boolean verbose() {
78          return cVerbose.get();
79      }
80  
81      //-----------------------------------------------------------------------
82      /**
83       * Launches the ZoneInfoCompiler tool.
84       *
85       * <pre>
86       * Usage: java org.joda.time.tz.ZoneInfoCompiler &lt;options&gt; &lt;source files&gt;
87       * where possible options include:
88       *   -src &lt;directory&gt;    Specify where to read source files
89       *   -dst &lt;directory&gt;    Specify where to write generated files
90       *   -verbose            Output verbosely (default false)
91       * </pre>
92       */
93      public static void main(String[] args) throws Exception {
94          if (args.length == 0) {
95              printUsage();
96              return;
97          }
98  
99          File inputDir = null;
100         File outputDir = null;
101         boolean verbose = false;
102 
103         int i;
104         for (i=0; i<args.length; i++) {
105             try {
106                 if ("-src".equals(args[i])) {
107                     inputDir = new File(args[++i]);
108                 } else if ("-dst".equals(args[i])) {
109                     outputDir = new File(args[++i]);
110                 } else if ("-verbose".equals(args[i])) {
111                     verbose = true;
112                 } else if ("-?".equals(args[i])) {
113                     printUsage();
114                     return;
115                 } else {
116                     break;
117                 }
118             } catch (IndexOutOfBoundsException e) {
119                 printUsage();
120                 return;
121             }
122         }
123 
124         if (i >= args.length) {
125             printUsage();
126             return;
127         }
128 
129         File[] sources = new File[args.length - i];
130         for (int j=0; i<args.length; i++,j++) {
131             sources[j] = inputDir == null ? new File(args[i]) : new File(inputDir, args[i]);
132         }
133 
134         cVerbose.set(verbose);
135         ZoneInfoCompiler zic = new ZoneInfoCompiler();
136         zic.compile(outputDir, sources);
137     }
138 
139     private static void printUsage() {
140         System.out.println("Usage: java org.joda.time.tz.ZoneInfoCompiler <options> <source files>");
141         System.out.println("where possible options include:");
142         System.out.println("  -src <directory>    Specify where to read source files");
143         System.out.println("  -dst <directory>    Specify where to write generated files");
144         System.out.println("  -verbose            Output verbosely (default false)");
145     }
146 
147     static DateTimeOfYear getStartOfYear() {
148         if (cStartOfYear == null) {
149             cStartOfYear = new DateTimeOfYear();
150         }
151         return cStartOfYear;
152     }
153 
154     static Chronology getLenientISOChronology() {
155         if (cLenientISO == null) {
156             cLenientISO = LenientChronology.getInstance(ISOChronology.getInstanceUTC());
157         }
158         return cLenientISO;
159     }
160 
161     /**
162      * @param zimap maps string ids to DateTimeZone objects.
163      */
164     static void writeZoneInfoMap(DataOutputStream dout, Map<String, DateTimeZone> zimap) throws IOException {
165         // Build the string pool.
166         Map<String, Short> idToIndex = new HashMap<String, Short>(zimap.size());
167         TreeMap<Short, String> indexToId = new TreeMap<Short, String>();
168 
169         short count = 0;
170         for (Entry<String, DateTimeZone> entry : zimap.entrySet()) {
171             String id = (String)entry.getKey();
172             if (!idToIndex.containsKey(id)) {
173                 Short index = Short.valueOf(count);
174                 idToIndex.put(id, index);
175                 indexToId.put(index, id);
176                 if (++count == 0) {
177                     throw new InternalError("Too many time zone ids");
178                 }
179             }
180             id = ((DateTimeZone)entry.getValue()).getID();
181             if (!idToIndex.containsKey(id)) {
182                 Short index = Short.valueOf(count);
183                 idToIndex.put(id, index);
184                 indexToId.put(index, id);
185                 if (++count == 0) {
186                     throw new InternalError("Too many time zone ids");
187                 }
188             }
189         }
190 
191         // Write the string pool, ordered by index.
192         dout.writeShort(indexToId.size());
193         for (String id : indexToId.values()) {
194             dout.writeUTF(id);
195         }
196 
197         // Write the mappings.
198         dout.writeShort(zimap.size());
199         for (Entry<String, DateTimeZone> entry : zimap.entrySet()) {
200             String id = entry.getKey();
201             dout.writeShort(idToIndex.get(id).shortValue());
202             id = entry.getValue().getID();
203             dout.writeShort(idToIndex.get(id).shortValue());
204         }
205     }
206 
207     static int parseYear(String str, int def) {
208         str = str.toLowerCase();
209         if (str.equals("minimum") || str.equals("min")) {
210             return Integer.MIN_VALUE;
211         } else if (str.equals("maximum") || str.equals("max")) {
212             return Integer.MAX_VALUE;
213         } else if (str.equals("only")) {
214             return def;
215         }
216         return Integer.parseInt(str);
217     }
218 
219     static int parseMonth(String str) {
220         DateTimeField field = ISOChronology.getInstanceUTC().monthOfYear();
221         return field.get(field.set(0, str, Locale.ENGLISH));
222     }
223 
224     static int parseDayOfWeek(String str) {
225         DateTimeField field = ISOChronology.getInstanceUTC().dayOfWeek();
226         return field.get(field.set(0, str, Locale.ENGLISH));
227     }
228     
229     static String parseOptional(String str) {
230         return (str.equals("-")) ? null : str;
231     }
232 
233     static int parseTime(String str) {
234         DateTimeFormatter p = ISODateTimeFormat.hourMinuteSecondFraction();
235         MutableDateTime mdt = new MutableDateTime(0, getLenientISOChronology());
236         int pos = 0;
237         if (str.startsWith("-")) {
238             pos = 1;
239         }
240         int newPos = p.parseInto(mdt, str, pos);
241         if (newPos == ~pos) {
242             throw new IllegalArgumentException(str);
243         }
244         int millis = (int)mdt.getMillis();
245         if (pos == 1) {
246             millis = -millis;
247         }
248         return millis;
249     }
250 
251     static char parseZoneChar(char c) {
252         switch (c) {
253         case 's': case 'S':
254             // Standard time
255             return 's';
256         case 'u': case 'U': case 'g': case 'G': case 'z': case 'Z':
257             // UTC
258             return 'u';
259         case 'w': case 'W': default:
260             // Wall time
261             return 'w';
262         }
263     }
264 
265     /**
266      * @return false if error.
267      */
268     static boolean test(String id, DateTimeZone tz) {
269         if (!id.equals(tz.getID())) {
270             return true;
271         }
272 
273         // Test to ensure that reported transitions are not duplicated.
274 
275         long millis = ISOChronology.getInstanceUTC().year().set(0, 1850);
276         long end = ISOChronology.getInstanceUTC().year().set(0, 2050);
277 
278         int offset = tz.getOffset(millis);
279         String key = tz.getNameKey(millis);
280 
281         List<Long> transitions = new ArrayList<Long>();
282 
283         while (true) {
284             long next = tz.nextTransition(millis);
285             if (next == millis || next > end) {
286                 break;
287             }
288 
289             millis = next;
290 
291             int nextOffset = tz.getOffset(millis);
292             String nextKey = tz.getNameKey(millis);
293 
294             if (offset == nextOffset
295                 && key.equals(nextKey)) {
296                 System.out.println("*d* Error in " + tz.getID() + " "
297                                    + new DateTime(millis,
298                                                   ISOChronology.getInstanceUTC()));
299                 return false;
300             }
301 
302             if (nextKey == null || (nextKey.length() < 3 && !"??".equals(nextKey))) {
303                 System.out.println("*s* Error in " + tz.getID() + " "
304                                    + new DateTime(millis,
305                                                   ISOChronology.getInstanceUTC())
306                                    + ", nameKey=" + nextKey);
307                 return false;
308             }
309 
310             transitions.add(Long.valueOf(millis));
311 
312             offset = nextOffset;
313             key = nextKey;
314         }
315 
316         // Now verify that reverse transitions match up.
317 
318         millis = ISOChronology.getInstanceUTC().year().set(0, 2050);
319         end = ISOChronology.getInstanceUTC().year().set(0, 1850);
320 
321         for (int i=transitions.size(); --i>= 0; ) {
322             long prev = tz.previousTransition(millis);
323             if (prev == millis || prev < end) {
324                 break;
325             }
326 
327             millis = prev;
328 
329             long trans = transitions.get(i).longValue();
330             
331             if (trans - 1 != millis) {
332                 System.out.println("*r* Error in " + tz.getID() + " "
333                                    + new DateTime(millis,
334                                                   ISOChronology.getInstanceUTC()) + " != "
335                                    + new DateTime(trans - 1,
336                                                   ISOChronology.getInstanceUTC()));
337                                    
338                 return false;
339             }
340         }
341 
342         return true;
343     }
344 
345     // Maps names to RuleSets.
346     private Map<String, RuleSet> iRuleSets;
347 
348     // List of Zone objects.
349     private List<Zone> iZones;
350 
351     // List String pairs to link.
352     private List<String> iLinks;
353 
354     public ZoneInfoCompiler() {
355         iRuleSets = new HashMap<String, RuleSet>();
356         iZones = new ArrayList<Zone>();
357         iLinks = new ArrayList<String>();
358     }
359 
360     /**
361      * Returns a map of ids to DateTimeZones.
362      *
363      * @param outputDir optional directory to write compiled data files to
364      * @param sources optional list of source files to parse
365      */
366     public Map<String, DateTimeZone> compile(File outputDir, File[] sources) throws IOException {
367         if (sources != null) {
368             for (int i=0; i<sources.length; i++) {
369                 BufferedReader in = new BufferedReader(new FileReader(sources[i]));
370                 parseDataFile(in);
371                 in.close();
372             }
373         }
374 
375         if (outputDir != null) {
376             if (!outputDir.exists()) {
377                 if (!outputDir.mkdirs()) {
378                     throw new IOException("Destination directory doesn't exist and cannot be created: " + outputDir);
379                 }
380             }
381             if (!outputDir.isDirectory()) {
382                 throw new IOException("Destination is not a directory: " + outputDir);
383             }
384         }
385 
386         Map<String, DateTimeZone> map = new TreeMap<String, DateTimeZone>();
387 
388         System.out.println("Writing zoneinfo files");
389         for (int i=0; i<iZones.size(); i++) {
390             Zone zone = iZones.get(i);
391             DateTimeZoneBuilder builder = new DateTimeZoneBuilder();
392             zone.addToBuilder(builder, iRuleSets);
393             final DateTimeZone original = builder.toDateTimeZone(zone.iName, true);
394             DateTimeZone tz = original;
395             if (test(tz.getID(), tz)) {
396                 map.put(tz.getID(), tz);
397                 if (outputDir != null) {
398                     if (ZoneInfoCompiler.verbose()) {
399                         System.out.println("Writing " + tz.getID());
400                     }
401                     File file = new File(outputDir, tz.getID());
402                     if (!file.getParentFile().exists()) {
403                         file.getParentFile().mkdirs();
404                     }
405                     OutputStream out = new FileOutputStream(file);
406                     try {
407                         builder.writeTo(zone.iName, out);
408                     } finally {
409                         out.close();
410                     }
411 
412                     // Test if it can be read back.
413                     InputStream in = new FileInputStream(file);
414                     DateTimeZone tz2 = DateTimeZoneBuilder.readFrom(in, tz.getID());
415                     in.close();
416 
417                     if (!original.equals(tz2)) {
418                         System.out.println("*e* Error in " + tz.getID() +
419                                            ": Didn't read properly from file");
420                     }
421                 }
422             }
423         }
424 
425         for (int pass=0; pass<2; pass++) {
426             for (int i=0; i<iLinks.size(); i += 2) {
427                 String id = iLinks.get(i);
428                 String alias = iLinks.get(i + 1);
429                 DateTimeZone tz = map.get(id);
430                 if (tz == null) {
431                     if (pass > 0) {
432                         System.out.println("Cannot find time zone '" + id +
433                                            "' to link alias '" + alias + "' to");
434                     }
435                 } else {
436                     map.put(alias, tz);
437                 }
438             }
439         }
440 
441         if (outputDir != null) {
442             System.out.println("Writing ZoneInfoMap");
443             File file = new File(outputDir, "ZoneInfoMap");
444             if (!file.getParentFile().exists()) {
445                 file.getParentFile().mkdirs();
446             }
447 
448             OutputStream out = new FileOutputStream(file);
449             DataOutputStream dout = new DataOutputStream(out);
450             try {
451                 // Sort and filter out any duplicates that match case.
452                 Map<String, DateTimeZone> zimap = new TreeMap<String, DateTimeZone>(String.CASE_INSENSITIVE_ORDER);
453                 zimap.putAll(map);
454                 writeZoneInfoMap(dout, zimap);
455             } finally {
456                 dout.close();
457             }
458         }
459 
460         return map;
461     }
462 
463     public void parseDataFile(BufferedReader in) throws IOException {
464         Zone zone = null;
465         String line;
466         while ((line = in.readLine()) != null) {
467             String trimmed = line.trim();
468             if (trimmed.length() == 0 || trimmed.charAt(0) == '#') {
469                 continue;
470             }
471 
472             int index = line.indexOf('#');
473             if (index >= 0) {
474                 line = line.substring(0, index);
475             }
476 
477             //System.out.println(line);
478 
479             StringTokenizer st = new StringTokenizer(line, " \t");
480 
481             if (Character.isWhitespace(line.charAt(0)) && st.hasMoreTokens()) {
482                 if (zone != null) {
483                     // Zone continuation
484                     zone.chain(st);
485                 }
486                 continue;
487             } else {
488                 if (zone != null) {
489                     iZones.add(zone);
490                 }
491                 zone = null;
492             }
493 
494             if (st.hasMoreTokens()) {
495                 String token = st.nextToken();
496                 if (token.equalsIgnoreCase("Rule")) {
497                     Rule r = new Rule(st);
498                     RuleSet rs = iRuleSets.get(r.iName);
499                     if (rs == null) {
500                         rs = new RuleSet(r);
501                         iRuleSets.put(r.iName, rs);
502                     } else {
503                         rs.addRule(r);
504                     }
505                 } else if (token.equalsIgnoreCase("Zone")) {
506                     zone = new Zone(st);
507                 } else if (token.equalsIgnoreCase("Link")) {
508                     iLinks.add(st.nextToken());
509                     iLinks.add(st.nextToken());
510                 } else {
511                     System.out.println("Unknown line: " + line);
512                 }
513             }
514         }
515 
516         if (zone != null) {
517             iZones.add(zone);
518         }
519     }
520 
521     static class DateTimeOfYear {
522         public final int iMonthOfYear;
523         public final int iDayOfMonth;
524         public final int iDayOfWeek;
525         public final boolean iAdvanceDayOfWeek;
526         public final int iMillisOfDay;
527         public final char iZoneChar;
528 
529         DateTimeOfYear() {
530             iMonthOfYear = 1;
531             iDayOfMonth = 1;
532             iDayOfWeek = 0;
533             iAdvanceDayOfWeek = false;
534             iMillisOfDay = 0;
535             iZoneChar = 'w';
536         }
537 
538         DateTimeOfYear(StringTokenizer st) {
539             int month = 1;
540             int day = 1;
541             int dayOfWeek = 0;
542             int millis = 0;
543             boolean advance = false;
544             char zoneChar = 'w';
545 
546             if (st.hasMoreTokens()) {
547                 month = parseMonth(st.nextToken());
548 
549                 if (st.hasMoreTokens()) {
550                     String str = st.nextToken();
551                     if (str.startsWith("last")) {
552                         day = -1;
553                         dayOfWeek = parseDayOfWeek(str.substring(4));
554                         advance = false;
555                     } else {
556                         try {
557                             day = Integer.parseInt(str);
558                             dayOfWeek = 0;
559                             advance = false;
560                         } catch (NumberFormatException e) {
561                             int index = str.indexOf(">=");
562                             if (index > 0) {
563                                 day = Integer.parseInt(str.substring(index + 2));
564                                 dayOfWeek = parseDayOfWeek(str.substring(0, index));
565                                 advance = true;
566                             } else {
567                                 index = str.indexOf("<=");
568                                 if (index > 0) {
569                                     day = Integer.parseInt(str.substring(index + 2));
570                                     dayOfWeek = parseDayOfWeek(str.substring(0, index));
571                                     advance = false;
572                                 } else {
573                                     throw new IllegalArgumentException(str);
574                                 }
575                             }
576                         }
577                     }
578 
579                     if (st.hasMoreTokens()) {
580                         str = st.nextToken();
581                         zoneChar = parseZoneChar(str.charAt(str.length() - 1));
582                         if (str.equals("24:00")) {
583                             LocalDate date = (day == -1 ?
584                                     new LocalDate(2001, month, 1).plusMonths(1) :
585                                     new LocalDate(2001, month, day).plusDays(1));
586                             advance = (day != -1);
587                             month = date.getMonthOfYear();
588                             day = date.getDayOfMonth();
589                             dayOfWeek = ((dayOfWeek - 1 + 1) % 7) + 1;
590                         } else {
591                             millis = parseTime(str);
592                         }
593                     }
594                 }
595             }
596 
597             iMonthOfYear = month;
598             iDayOfMonth = day;
599             iDayOfWeek = dayOfWeek;
600             iAdvanceDayOfWeek = advance;
601             iMillisOfDay = millis;
602             iZoneChar = zoneChar;
603         }
604 
605         /**
606          * Adds a recurring savings rule to the builder.
607          */
608         public void addRecurring(DateTimeZoneBuilder builder, String nameKey,
609                                  int saveMillis, int fromYear, int toYear)
610         {
611             builder.addRecurringSavings(nameKey, saveMillis,
612                                         fromYear, toYear,
613                                         iZoneChar,
614                                         iMonthOfYear,
615                                         iDayOfMonth,
616                                         iDayOfWeek,
617                                         iAdvanceDayOfWeek,
618                                         iMillisOfDay);
619         }
620 
621         /**
622          * Adds a cutover to the builder.
623          */
624         public void addCutover(DateTimeZoneBuilder builder, int year) {
625             builder.addCutover(year,
626                                iZoneChar,
627                                iMonthOfYear,
628                                iDayOfMonth,
629                                iDayOfWeek,
630                                iAdvanceDayOfWeek,
631                                iMillisOfDay);
632         }
633 
634         public String toString() {
635             return
636                 "MonthOfYear: " + iMonthOfYear + "\n" +
637                 "DayOfMonth: " + iDayOfMonth + "\n" +
638                 "DayOfWeek: " + iDayOfWeek + "\n" +
639                 "AdvanceDayOfWeek: " + iAdvanceDayOfWeek + "\n" +
640                 "MillisOfDay: " + iMillisOfDay + "\n" +
641                 "ZoneChar: " + iZoneChar + "\n";
642         }
643     }
644 
645     private static class Rule {
646         public final String iName;
647         public final int iFromYear;
648         public final int iToYear;
649         public final String iType;
650         public final DateTimeOfYear iDateTimeOfYear;
651         public final int iSaveMillis;
652         public final String iLetterS;
653 
654         Rule(StringTokenizer st) {
655             iName = st.nextToken().intern();
656             iFromYear = parseYear(st.nextToken(), 0);
657             iToYear = parseYear(st.nextToken(), iFromYear);
658             if (iToYear < iFromYear) {
659                 throw new IllegalArgumentException();
660             }
661             iType = parseOptional(st.nextToken());
662             iDateTimeOfYear = new DateTimeOfYear(st);
663             iSaveMillis = parseTime(st.nextToken());
664             iLetterS = parseOptional(st.nextToken());
665         }
666 
667         /**
668          * Adds a recurring savings rule to the builder.
669          */
670         public void addRecurring(DateTimeZoneBuilder builder, String nameFormat) {
671             String nameKey = formatName(nameFormat);
672             iDateTimeOfYear.addRecurring
673                 (builder, nameKey, iSaveMillis, iFromYear, iToYear);
674         }
675 
676         private String formatName(String nameFormat) {
677             int index = nameFormat.indexOf('/');
678             if (index > 0) {
679                 if (iSaveMillis == 0) {
680                     // Extract standard name.
681                     return nameFormat.substring(0, index).intern();
682                 } else {
683                     return nameFormat.substring(index + 1).intern();
684                 }
685             }
686             index = nameFormat.indexOf("%s");
687             if (index < 0) {
688                 return nameFormat;
689             }
690             String left = nameFormat.substring(0, index);
691             String right = nameFormat.substring(index + 2);
692             String name;
693             if (iLetterS == null) {
694                 name = left.concat(right);
695             } else {
696                 name = left + iLetterS + right;
697             }
698             return name.intern();
699         }
700 
701         public String toString() {
702             return
703                 "[Rule]\n" + 
704                 "Name: " + iName + "\n" +
705                 "FromYear: " + iFromYear + "\n" +
706                 "ToYear: " + iToYear + "\n" +
707                 "Type: " + iType + "\n" +
708                 iDateTimeOfYear +
709                 "SaveMillis: " + iSaveMillis + "\n" +
710                 "LetterS: " + iLetterS + "\n";
711         }
712     }
713 
714     private static class RuleSet {
715         private List<Rule> iRules;
716 
717         RuleSet(Rule rule) {
718             iRules = new ArrayList<Rule>();
719             iRules.add(rule);
720         }
721 
722         void addRule(Rule rule) {
723             if (!(rule.iName.equals(iRules.get(0).iName))) {
724                 throw new IllegalArgumentException("Rule name mismatch");
725             }
726             iRules.add(rule);
727         }
728 
729         /**
730          * Adds recurring savings rules to the builder.
731          */
732         public void addRecurring(DateTimeZoneBuilder builder, String nameFormat) {
733             for (int i=0; i<iRules.size(); i++) {
734                 Rule rule = iRules.get(i);
735                 rule.addRecurring(builder, nameFormat);
736             }
737         }
738     }
739 
740     private static class Zone {
741         public final String iName;
742         public final int iOffsetMillis;
743         public final String iRules;
744         public final String iFormat;
745         public final int iUntilYear;
746         public final DateTimeOfYear iUntilDateTimeOfYear;
747 
748         private Zone iNext;
749 
750         Zone(StringTokenizer st) {
751             this(st.nextToken(), st);
752         }
753 
754         private Zone(String name, StringTokenizer st) {
755             iName = name.intern();
756             iOffsetMillis = parseTime(st.nextToken());
757             iRules = parseOptional(st.nextToken());
758             iFormat = st.nextToken().intern();
759 
760             int year = Integer.MAX_VALUE;
761             DateTimeOfYear dtOfYear = getStartOfYear();
762 
763             if (st.hasMoreTokens()) {
764                 year = Integer.parseInt(st.nextToken());
765                 if (st.hasMoreTokens()) {
766                     dtOfYear = new DateTimeOfYear(st);
767                 }
768             }
769 
770             iUntilYear = year;
771             iUntilDateTimeOfYear = dtOfYear;
772         }
773 
774         void chain(StringTokenizer st) {
775             if (iNext != null) {
776                 iNext.chain(st);
777             } else {
778                 iNext = new Zone(iName, st);
779             }
780         }
781 
782         /*
783         public DateTimeZone buildDateTimeZone(Map ruleSets) {
784             DateTimeZoneBuilder builder = new DateTimeZoneBuilder();
785             addToBuilder(builder, ruleSets);
786             return builder.toDateTimeZone(iName);
787         }
788         */
789 
790         /**
791          * Adds zone info to the builder.
792          */
793         public void addToBuilder(DateTimeZoneBuilder builder, Map<String, RuleSet> ruleSets) {
794             addToBuilder(this, builder, ruleSets);
795         }
796 
797         private static void addToBuilder(Zone zone,
798                                          DateTimeZoneBuilder builder,
799                                          Map<String, RuleSet> ruleSets)
800         {
801             for (; zone != null; zone = zone.iNext) {
802                 builder.setStandardOffset(zone.iOffsetMillis);
803 
804                 if (zone.iRules == null) {
805                     builder.setFixedSavings(zone.iFormat, 0);
806                 } else {
807                     try {
808                         // Check if iRules actually just refers to a savings.
809                         int saveMillis = parseTime(zone.iRules);
810                         builder.setFixedSavings(zone.iFormat, saveMillis);
811                     }
812                     catch (Exception e) {
813                         RuleSet rs = ruleSets.get(zone.iRules);
814                         if (rs == null) {
815                             throw new IllegalArgumentException
816                                 ("Rules not found: " + zone.iRules);
817                         }
818                         rs.addRecurring(builder, zone.iFormat);
819                     }
820                 }
821 
822                 if (zone.iUntilYear == Integer.MAX_VALUE) {
823                     break;
824                 }
825 
826                 zone.iUntilDateTimeOfYear.addCutover(builder, zone.iUntilYear);
827             }
828         }
829 
830         public String toString() {
831             String str =
832                 "[Zone]\n" + 
833                 "Name: " + iName + "\n" +
834                 "OffsetMillis: " + iOffsetMillis + "\n" +
835                 "Rules: " + iRules + "\n" +
836                 "Format: " + iFormat + "\n" +
837                 "UntilYear: " + iUntilYear + "\n" +
838                 iUntilDateTimeOfYear;
839 
840             if (iNext == null) {
841                 return str;
842             }
843 
844             return str + "...\n" + iNext.toString();
845         }
846     }
847 }
848