001/*
002 *  Copyright 2001-2013 Stephen Colebourne
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.joda.time.tz;
017
018import java.io.BufferedReader;
019import java.io.DataOutputStream;
020import java.io.File;
021import java.io.FileInputStream;
022import java.io.FileOutputStream;
023import java.io.FileReader;
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.OutputStream;
027import java.util.ArrayList;
028import java.util.HashMap;
029import java.util.List;
030import java.util.Locale;
031import java.util.Map;
032import java.util.StringTokenizer;
033import java.util.TreeMap;
034import java.util.Map.Entry;
035
036import org.joda.time.Chronology;
037import org.joda.time.DateTime;
038import org.joda.time.DateTimeField;
039import org.joda.time.DateTimeZone;
040import org.joda.time.LocalDate;
041import org.joda.time.MutableDateTime;
042import org.joda.time.chrono.ISOChronology;
043import org.joda.time.chrono.LenientChronology;
044import org.joda.time.format.DateTimeFormatter;
045import org.joda.time.format.ISODateTimeFormat;
046
047/**
048 * Compiles Olson ZoneInfo database files into binary files for each time zone
049 * in the database. {@link DateTimeZoneBuilder} is used to construct and encode
050 * compiled data files. {@link ZoneInfoProvider} loads the encoded files and
051 * converts them back into {@link DateTimeZone} objects.
052 * <p>
053 * Although this tool is similar to zic, the binary formats are not
054 * compatible. The latest Olson database files may be obtained
055 * <a href="http://www.twinsun.com/tz/tz-link.htm">here</a>.
056 * <p>
057 * ZoneInfoCompiler is mutable and not thread-safe, although the main method
058 * may be safely invoked by multiple threads.
059 *
060 * @author Brian S O'Neill
061 * @since 1.0
062 */
063public class ZoneInfoCompiler {
064    static DateTimeOfYear cStartOfYear;
065
066    static Chronology cLenientISO;
067
068    static ThreadLocal<Boolean> cVerbose = new ThreadLocal<Boolean>();
069    static {
070        cVerbose.set(Boolean.FALSE);
071    }
072
073    /**
074     * Gets a flag indicating that verbose logging is required.
075     * @return true to log verbosely
076     */
077    public static boolean verbose() {
078        return cVerbose.get();
079    }
080
081    //-----------------------------------------------------------------------
082    /**
083     * Launches the ZoneInfoCompiler tool.
084     *
085     * <pre>
086     * Usage: java org.joda.time.tz.ZoneInfoCompiler &lt;options&gt; &lt;source files&gt;
087     * where possible options include:
088     *   -src &lt;directory&gt;    Specify where to read source files
089     *   -dst &lt;directory&gt;    Specify where to write generated files
090     *   -verbose            Output verbosely (default false)
091     * </pre>
092     */
093    public static void main(String[] args) throws Exception {
094        if (args.length == 0) {
095            printUsage();
096            return;
097        }
098
099        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