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     */
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                    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