001    /*
002     *  Copyright 2001-2010 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     */
016    package org.joda.time.tz;
017    
018    import java.io.BufferedReader;
019    import java.io.DataOutputStream;
020    import java.io.File;
021    import java.io.FileInputStream;
022    import java.io.FileOutputStream;
023    import java.io.FileReader;
024    import java.io.IOException;
025    import java.io.InputStream;
026    import java.io.OutputStream;
027    import java.util.ArrayList;
028    import java.util.HashMap;
029    import java.util.List;
030    import java.util.Locale;
031    import java.util.Map;
032    import java.util.StringTokenizer;
033    import java.util.TreeMap;
034    import java.util.Map.Entry;
035    
036    import org.joda.time.Chronology;
037    import org.joda.time.DateTime;
038    import org.joda.time.DateTimeField;
039    import org.joda.time.DateTimeZone;
040    import org.joda.time.LocalDate;
041    import org.joda.time.MutableDateTime;
042    import org.joda.time.chrono.ISOChronology;
043    import org.joda.time.chrono.LenientChronology;
044    import org.joda.time.format.DateTimeFormatter;
045    import 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     */
063    public 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                    throw new IOException("Destination directory doesn't exist: " + outputDir);
378                }
379                if (!outputDir.isDirectory()) {
380                    throw new IOException("Destination is not a directory: " + outputDir);
381                }
382            }
383    
384            Map<String, DateTimeZone> map = new TreeMap<String, DateTimeZone>();
385    
386            System.out.println("Writing zoneinfo files");
387            for (int i=0; i<iZones.size(); i++) {
388                Zone zone = iZones.get(i);
389                DateTimeZoneBuilder builder = new DateTimeZoneBuilder();
390                zone.addToBuilder(builder, iRuleSets);
391                final DateTimeZone original = builder.toDateTimeZone(zone.iName, true);
392                DateTimeZone tz = original;
393                if (test(tz.getID(), tz)) {
394                    map.put(tz.getID(), tz);
395                    if (outputDir != null) {
396                        if (ZoneInfoCompiler.verbose()) {
397                            System.out.println("Writing " + tz.getID());
398                        }
399                        File file = new File(outputDir, tz.getID());
400                        if (!file.getParentFile().exists()) {
401                            file.getParentFile().mkdirs();
402                        }
403                        OutputStream out = new FileOutputStream(file);
404                        try {
405                            builder.writeTo(zone.iName, out);
406                        } finally {
407                            out.close();
408                        }
409    
410                        // Test if it can be read back.
411                        InputStream in = new FileInputStream(file);
412                        DateTimeZone tz2 = DateTimeZoneBuilder.readFrom(in, tz.getID());
413                        in.close();
414    
415                        if (!original.equals(tz2)) {
416                            System.out.println("*e* Error in " + tz.getID() +
417                                               ": Didn't read properly from file");
418                        }
419                    }
420                }
421            }
422    
423            for (int pass=0; pass<2; pass++) {
424                for (int i=0; i<iLinks.size(); i += 2) {
425                    String id = iLinks.get(i);
426                    String alias = iLinks.get(i + 1);
427                    DateTimeZone tz = map.get(id);
428                    if (tz == null) {
429                        if (pass > 0) {
430                            System.out.println("Cannot find time zone '" + id +
431                                               "' to link alias '" + alias + "' to");
432                        }
433                    } else {
434                        map.put(alias, tz);
435                    }
436                }
437            }
438    
439            if (outputDir != null) {
440                System.out.println("Writing ZoneInfoMap");
441                File file = new File(outputDir, "ZoneInfoMap");
442                if (!file.getParentFile().exists()) {
443                    file.getParentFile().mkdirs();
444                }
445    
446                OutputStream out = new FileOutputStream(file);
447                DataOutputStream dout = new DataOutputStream(out);
448                try {
449                    // Sort and filter out any duplicates that match case.
450                    Map<String, DateTimeZone> zimap = new TreeMap<String, DateTimeZone>(String.CASE_INSENSITIVE_ORDER);
451                    zimap.putAll(map);
452                    writeZoneInfoMap(dout, zimap);
453                } finally {
454                    dout.close();
455                }
456            }
457    
458            return map;
459        }
460    
461        public void parseDataFile(BufferedReader in) throws IOException {
462            Zone zone = null;
463            String line;
464            while ((line = in.readLine()) != null) {
465                String trimmed = line.trim();
466                if (trimmed.length() == 0 || trimmed.charAt(0) == '#') {
467                    continue;
468                }
469    
470                int index = line.indexOf('#');
471                if (index >= 0) {
472                    line = line.substring(0, index);
473                }
474    
475                //System.out.println(line);
476    
477                StringTokenizer st = new StringTokenizer(line, " \t");
478    
479                if (Character.isWhitespace(line.charAt(0)) && st.hasMoreTokens()) {
480                    if (zone != null) {
481                        // Zone continuation
482                        zone.chain(st);
483                    }
484                    continue;
485                } else {
486                    if (zone != null) {
487                        iZones.add(zone);
488                    }
489                    zone = null;
490                }
491    
492                if (st.hasMoreTokens()) {
493                    String token = st.nextToken();
494                    if (token.equalsIgnoreCase("Rule")) {
495                        Rule r = new Rule(st);
496                        RuleSet rs = iRuleSets.get(r.iName);
497                        if (rs == null) {
498                            rs = new RuleSet(r);
499                            iRuleSets.put(r.iName, rs);
500                        } else {
501                            rs.addRule(r);
502                        }
503                    } else if (token.equalsIgnoreCase("Zone")) {
504                        zone = new Zone(st);
505                    } else if (token.equalsIgnoreCase("Link")) {
506                        iLinks.add(st.nextToken());
507                        iLinks.add(st.nextToken());
508                    } else {
509                        System.out.println("Unknown line: " + line);
510                    }
511                }
512            }
513    
514            if (zone != null) {
515                iZones.add(zone);
516            }
517        }
518    
519        static class DateTimeOfYear {
520            public final int iMonthOfYear;
521            public final int iDayOfMonth;
522            public final int iDayOfWeek;
523            public final boolean iAdvanceDayOfWeek;
524            public final int iMillisOfDay;
525            public final char iZoneChar;
526    
527            DateTimeOfYear() {
528                iMonthOfYear = 1;
529                iDayOfMonth = 1;
530                iDayOfWeek = 0;
531                iAdvanceDayOfWeek = false;
532                iMillisOfDay = 0;
533                iZoneChar = 'w';
534            }
535    
536            DateTimeOfYear(StringTokenizer st) {
537                int month = 1;
538                int day = 1;
539                int dayOfWeek = 0;
540                int millis = 0;
541                boolean advance = false;
542                char zoneChar = 'w';
543    
544                if (st.hasMoreTokens()) {
545                    month = parseMonth(st.nextToken());
546    
547                    if (st.hasMoreTokens()) {
548                        String str = st.nextToken();
549                        if (str.startsWith("last")) {
550                            day = -1;
551                            dayOfWeek = parseDayOfWeek(str.substring(4));
552                            advance = false;
553                        } else {
554                            try {
555                                day = Integer.parseInt(str);
556                                dayOfWeek = 0;
557                                advance = false;
558                            } catch (NumberFormatException e) {
559                                int index = str.indexOf(">=");
560                                if (index > 0) {
561                                    day = Integer.parseInt(str.substring(index + 2));
562                                    dayOfWeek = parseDayOfWeek(str.substring(0, index));
563                                    advance = true;
564                                } else {
565                                    index = str.indexOf("<=");
566                                    if (index > 0) {
567                                        day = Integer.parseInt(str.substring(index + 2));
568                                        dayOfWeek = parseDayOfWeek(str.substring(0, index));
569                                        advance = false;
570                                    } else {
571                                        throw new IllegalArgumentException(str);
572                                    }
573                                }
574                            }
575                        }
576    
577                        if (st.hasMoreTokens()) {
578                            str = st.nextToken();
579                            zoneChar = parseZoneChar(str.charAt(str.length() - 1));
580                            if (str.equals("24:00")) {
581                                LocalDate date = (day == -1 ?
582                                        new LocalDate(2001, month, 1).plusMonths(1) :
583                                        new LocalDate(2001, month, day).plusDays(1));
584                                advance = (day != -1);
585                                month = date.getMonthOfYear();
586                                day = date.getDayOfMonth();
587                                dayOfWeek = ((dayOfWeek - 1 + 1) % 7) + 1;
588                            } else {
589                                millis = parseTime(str);
590                            }
591                        }
592                    }
593                }
594    
595                iMonthOfYear = month;
596                iDayOfMonth = day;
597                iDayOfWeek = dayOfWeek;
598                iAdvanceDayOfWeek = advance;
599                iMillisOfDay = millis;
600                iZoneChar = zoneChar;
601            }
602    
603            /**
604             * Adds a recurring savings rule to the builder.
605             */
606            public void addRecurring(DateTimeZoneBuilder builder, String nameKey,
607                                     int saveMillis, int fromYear, int toYear)
608            {
609                builder.addRecurringSavings(nameKey, saveMillis,
610                                            fromYear, toYear,
611                                            iZoneChar,
612                                            iMonthOfYear,
613                                            iDayOfMonth,
614                                            iDayOfWeek,
615                                            iAdvanceDayOfWeek,
616                                            iMillisOfDay);
617            }
618    
619            /**
620             * Adds a cutover to the builder.
621             */
622            public void addCutover(DateTimeZoneBuilder builder, int year) {
623                builder.addCutover(year,
624                                   iZoneChar,
625                                   iMonthOfYear,
626                                   iDayOfMonth,
627                                   iDayOfWeek,
628                                   iAdvanceDayOfWeek,
629                                   iMillisOfDay);
630            }
631    
632            public String toString() {
633                return
634                    "MonthOfYear: " + iMonthOfYear + "\n" +
635                    "DayOfMonth: " + iDayOfMonth + "\n" +
636                    "DayOfWeek: " + iDayOfWeek + "\n" +
637                    "AdvanceDayOfWeek: " + iAdvanceDayOfWeek + "\n" +
638                    "MillisOfDay: " + iMillisOfDay + "\n" +
639                    "ZoneChar: " + iZoneChar + "\n";
640            }
641        }
642    
643        private static class Rule {
644            public final String iName;
645            public final int iFromYear;
646            public final int iToYear;
647            public final String iType;
648            public final DateTimeOfYear iDateTimeOfYear;
649            public final int iSaveMillis;
650            public final String iLetterS;
651    
652            Rule(StringTokenizer st) {
653                iName = st.nextToken().intern();
654                iFromYear = parseYear(st.nextToken(), 0);
655                iToYear = parseYear(st.nextToken(), iFromYear);
656                if (iToYear < iFromYear) {
657                    throw new IllegalArgumentException();
658                }
659                iType = parseOptional(st.nextToken());
660                iDateTimeOfYear = new DateTimeOfYear(st);
661                iSaveMillis = parseTime(st.nextToken());
662                iLetterS = parseOptional(st.nextToken());
663            }
664    
665            /**
666             * Adds a recurring savings rule to the builder.
667             */
668            public void addRecurring(DateTimeZoneBuilder builder, String nameFormat) {
669                String nameKey = formatName(nameFormat);
670                iDateTimeOfYear.addRecurring
671                    (builder, nameKey, iSaveMillis, iFromYear, iToYear);
672            }
673    
674            private String formatName(String nameFormat) {
675                int index = nameFormat.indexOf('/');
676                if (index > 0) {
677                    if (iSaveMillis == 0) {
678                        // Extract standard name.
679                        return nameFormat.substring(0, index).intern();
680                    } else {
681                        return nameFormat.substring(index + 1).intern();
682                    }
683                }
684                index = nameFormat.indexOf("%s");
685                if (index < 0) {
686                    return nameFormat;
687                }
688                String left = nameFormat.substring(0, index);
689                String right = nameFormat.substring(index + 2);
690                String name;
691                if (iLetterS == null) {
692                    name = left.concat(right);
693                } else {
694                    name = left + iLetterS + right;
695                }
696                return name.intern();
697            }
698    
699            public String toString() {
700                return
701                    "[Rule]\n" + 
702                    "Name: " + iName + "\n" +
703                    "FromYear: " + iFromYear + "\n" +
704                    "ToYear: " + iToYear + "\n" +
705                    "Type: " + iType + "\n" +
706                    iDateTimeOfYear +
707                    "SaveMillis: " + iSaveMillis + "\n" +
708                    "LetterS: " + iLetterS + "\n";
709            }
710        }
711    
712        private static class RuleSet {
713            private List<Rule> iRules;
714    
715            RuleSet(Rule rule) {
716                iRules = new ArrayList<Rule>();
717                iRules.add(rule);
718            }
719    
720            void addRule(Rule rule) {
721                if (!(rule.iName.equals(iRules.get(0).iName))) {
722                    throw new IllegalArgumentException("Rule name mismatch");
723                }
724                iRules.add(rule);
725            }
726    
727            /**
728             * Adds recurring savings rules to the builder.
729             */
730            public void addRecurring(DateTimeZoneBuilder builder, String nameFormat) {
731                for (int i=0; i<iRules.size(); i++) {
732                    Rule rule = iRules.get(i);
733                    rule.addRecurring(builder, nameFormat);
734                }
735            }
736        }
737    
738        private static class Zone {
739            public final String iName;
740            public final int iOffsetMillis;
741            public final String iRules;
742            public final String iFormat;
743            public final int iUntilYear;
744            public final DateTimeOfYear iUntilDateTimeOfYear;
745    
746            private Zone iNext;
747    
748            Zone(StringTokenizer st) {
749                this(st.nextToken(), st);
750            }
751    
752            private Zone(String name, StringTokenizer st) {
753                iName = name.intern();
754                iOffsetMillis = parseTime(st.nextToken());
755                iRules = parseOptional(st.nextToken());
756                iFormat = st.nextToken().intern();
757    
758                int year = Integer.MAX_VALUE;
759                DateTimeOfYear dtOfYear = getStartOfYear();
760    
761                if (st.hasMoreTokens()) {
762                    year = Integer.parseInt(st.nextToken());
763                    if (st.hasMoreTokens()) {
764                        dtOfYear = new DateTimeOfYear(st);
765                    }
766                }
767    
768                iUntilYear = year;
769                iUntilDateTimeOfYear = dtOfYear;
770            }
771    
772            void chain(StringTokenizer st) {
773                if (iNext != null) {
774                    iNext.chain(st);
775                } else {
776                    iNext = new Zone(iName, st);
777                }
778            }
779    
780            /*
781            public DateTimeZone buildDateTimeZone(Map ruleSets) {
782                DateTimeZoneBuilder builder = new DateTimeZoneBuilder();
783                addToBuilder(builder, ruleSets);
784                return builder.toDateTimeZone(iName);
785            }
786            */
787    
788            /**
789             * Adds zone info to the builder.
790             */
791            public void addToBuilder(DateTimeZoneBuilder builder, Map<String, RuleSet> ruleSets) {
792                addToBuilder(this, builder, ruleSets);
793            }
794    
795            private static void addToBuilder(Zone zone,
796                                             DateTimeZoneBuilder builder,
797                                             Map<String, RuleSet> ruleSets)
798            {
799                for (; zone != null; zone = zone.iNext) {
800                    builder.setStandardOffset(zone.iOffsetMillis);
801    
802                    if (zone.iRules == null) {
803                        builder.setFixedSavings(zone.iFormat, 0);
804                    } else {
805                        try {
806                            // Check if iRules actually just refers to a savings.
807                            int saveMillis = parseTime(zone.iRules);
808                            builder.setFixedSavings(zone.iFormat, saveMillis);
809                        }
810                        catch (Exception e) {
811                            RuleSet rs = ruleSets.get(zone.iRules);
812                            if (rs == null) {
813                                throw new IllegalArgumentException
814                                    ("Rules not found: " + zone.iRules);
815                            }
816                            rs.addRecurring(builder, zone.iFormat);
817                        }
818                    }
819    
820                    if (zone.iUntilYear == Integer.MAX_VALUE) {
821                        break;
822                    }
823    
824                    zone.iUntilDateTimeOfYear.addCutover(builder, zone.iUntilYear);
825                }
826            }
827    
828            public String toString() {
829                String str =
830                    "[Zone]\n" + 
831                    "Name: " + iName + "\n" +
832                    "OffsetMillis: " + iOffsetMillis + "\n" +
833                    "Rules: " + iRules + "\n" +
834                    "Format: " + iFormat + "\n" +
835                    "UntilYear: " + iUntilYear + "\n" +
836                    iUntilDateTimeOfYear;
837    
838                if (iNext == null) {
839                    return str;
840                }
841    
842                return str + "...\n" + iNext.toString();
843            }
844        }
845    }
846