1/*
2 * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26import java.io.BufferedReader;
27import java.io.FileReader;
28import java.io.FileNotFoundException;
29import java.io.IOException;
30import java.util.HashMap;
31import java.util.List;
32import java.util.Map;
33import java.util.StringTokenizer;
34
35/**
36 * Zoneinfo provides javazic compiler front-end functionality.
37 * @since 1.4
38 */
39class Zoneinfo {
40
41    private static final int minYear = 1900;
42    private static final int maxYear = 2037;
43    private static final long minTime = Time.getLocalTime(minYear, Month.JANUARY, 1, 0);
44    private static int startYear = minYear;
45    private static int endYear = maxYear;
46
47    /**
48     * True if javazic should generate a list of SimpleTimeZone
49     * instances for the SimpleTimeZone-based time zone support.
50     */
51    static boolean isYearForTimeZoneDataSpecified = false;
52
53    /**
54     * Zone name to Zone mappings
55     */
56    private Map<String,Zone> zones;
57
58    /**
59     * Rule name to Rule mappings
60     */
61    private Map<String,Rule> rules;
62
63    /**
64     * Alias name to real name mappings
65     */
66    private Map<String,String> aliases;
67
68    /**
69     * Constracts a Zoneinfo.
70     */
71    Zoneinfo() {
72        zones = new HashMap<String,Zone>();
73        rules = new HashMap<String,Rule>();
74        aliases = new HashMap<String,String>();
75    }
76
77    /**
78     * Adds the given zone to the list of Zones.
79     * @param zone Zone to be added to the list.
80     */
81    void add(Zone zone) {
82        String name = zone.getName();
83        zones.put(name, zone);
84    }
85
86    /**
87     * Adds the given rule to the list of Rules.
88     * @param rule Rule to be added to the list.
89     */
90    void add(Rule rule) {
91        String name = rule.getName();
92        rules.put(name, rule);
93    }
94
95    /**
96     * Puts the specifid name pair to the alias table.
97     * @param name1 an alias time zone name
98     * @param name2 the real time zone of the alias name
99     */
100    void putAlias(String name1, String name2) {
101        aliases.put(name1, name2);
102    }
103
104    /**
105     * Sets the given year for SimpleTimeZone list output.
106     * This method is called when the -S option is specified.
107     * @param year the year for which SimpleTimeZone list should be generated
108     */
109    static void setYear(int year) {
110        setStartYear(year);
111        setEndYear(year);
112        isYearForTimeZoneDataSpecified = true;
113    }
114
115    /**
116     * Sets the start year.
117     * @param year the start year value
118     * @throws IllegalArgumentException if the specified year value is
119     * smaller than the minimum year or greater than the end year.
120     */
121    static void setStartYear(int year) {
122        if (year < minYear || year > endYear) {
123            throw new IllegalArgumentException("invalid start year specified: " + year);
124        }
125        startYear = year;
126    }
127
128    /**
129     * @return the start year value
130     */
131    static int getStartYear() {
132        return startYear;
133    }
134
135    /**
136     * Sets the end year.
137     * @param year the end year value
138     * @throws IllegalArgumentException if the specified year value is
139     * smaller than the start year or greater than the maximum year.
140     */
141    static void setEndYear(int year) {
142        if (year < startYear || year > maxYear) {
143            throw new IllegalArgumentException();
144        }
145        endYear = year;
146    }
147
148    /**
149     * @return the end year value
150     */
151    static int getEndYear() {
152        return endYear;
153    }
154
155    /**
156     * @return the minimum year value
157     */
158    static int getMinYear() {
159        return minYear;
160    }
161
162    /**
163     * @return the maximum year value
164     */
165    static int getMaxYear() {
166        return maxYear;
167    }
168
169    /**
170     * @return the alias table
171     */
172    Map<String,String> getAliases() {
173        return aliases;
174    }
175
176    /**
177     * @return the Zone list
178     */
179    Map<String,Zone> getZones() {
180        return zones;
181    }
182
183    /**
184     * @return a Zone specified by name.
185     * @param name a zone name
186     */
187    Zone getZone(String name) {
188        return zones.get(name);
189    }
190
191    /**
192     * @return a Rule specified by name.
193     * @param name a rule name
194     */
195    Rule getRule(String name) {
196        return rules.get(name);
197    }
198
199    private static String line;
200
201    private static int lineNum;
202
203    /**
204     * Parses the specified time zone data file and creates a Zoneinfo
205     * that has all Rules, Zones and Links (aliases) information.
206     * @param fname the time zone data file name
207     * @return a Zoneinfo object
208     */
209    static Zoneinfo parse(String fname) {
210        BufferedReader in = null;
211        try {
212            FileReader fr = new FileReader(fname);
213            in = new BufferedReader(fr);
214        } catch (FileNotFoundException e) {
215            panic("can't open file: "+fname);
216        }
217        Zoneinfo zi = new Zoneinfo();
218        boolean continued = false;
219        Zone zone = null;
220        String l;
221        lineNum = 0;
222
223        try {
224            while ((line = in.readLine()) != null) {
225                lineNum++;
226                // skip blank and comment lines
227                if (line.length() == 0 || line.charAt(0) == '#') {
228                    continue;
229                }
230
231                // trim trailing comments
232                int rindex = line.lastIndexOf('#');
233                if (rindex != -1) {
234                    // take the data part of the line
235                    l = line.substring(0, rindex);
236                } else {
237                    l = line;
238                }
239
240                StringTokenizer tokens = new StringTokenizer(l);
241                if (!tokens.hasMoreTokens()) {
242                    continue;
243                }
244                String token = tokens.nextToken();
245
246                if (continued || "Zone".equals(token)) {
247                    if (zone == null) {
248                        if (!tokens.hasMoreTokens()) {
249                            panic("syntax error: zone no more token");
250                        }
251                        token = tokens.nextToken();
252                        // if the zone name is in "GMT+hh" or "GMT-hh"
253                        // format, ignore it due to spec conflict.
254                        if (token.startsWith("GMT+") || token.startsWith("GMT-")) {
255                            continue;
256                        }
257                        zone = new Zone(token);
258                    } else {
259                        // no way to push the current token back...
260                        tokens = new StringTokenizer(l);
261                    }
262
263                    ZoneRec zrec = ZoneRec.parse(tokens);
264                    zrec.setLine(line);
265                    zone.add(zrec);
266                    if ((continued = zrec.hasUntil()) == false) {
267                        if (Zone.isTargetZone(zone.getName())) {
268                            // zone.resolve(zi);
269                            zi.add(zone);
270                        }
271                        zone = null;
272                    }
273                } else if ("Rule".equals(token)) {
274                    if (!tokens.hasMoreTokens()) {
275                        panic("syntax error: rule no more token");
276                    }
277                    token = tokens.nextToken();
278                    Rule rule = zi.getRule(token);
279                    if (rule == null) {
280                        rule = new Rule(token);
281                        zi.add(rule);
282                    }
283                    RuleRec rrec = RuleRec.parse(tokens);
284                    rrec.setLine(line);
285                    rule.add(rrec);
286                } else if ("Link".equals(token)) {
287                    // Link <newname> <oldname>
288                    try {
289                        String name1 = tokens.nextToken();
290                        String name2 = tokens.nextToken();
291
292                        // if the zone name is in "GMT+hh" or "GMT-hh"
293                        // format, ignore it due to spec conflict with
294                        // custom time zones. Also, ignore "ROC" for
295                        // PC-ness.
296                        if (name2.startsWith("GMT+") || name2.startsWith("GMT-")
297                            || "ROC".equals(name2)) {
298                            continue;
299                        }
300                        zi.putAlias(name2, name1);
301                    } catch (Exception e) {
302                        panic("syntax error: no more token for Link");
303                    }
304                }
305            }
306            in.close();
307        } catch (IOException ex) {
308            panic("IO error: " + ex.getMessage());
309        }
310
311        return zi;
312    }
313
314    /**
315     * Interprets a zone and constructs a Timezone object that
316     * contains enough information on GMT offsets and DST schedules to
317     * generate a zone info database.
318     *
319     * @param zoneName the zone name for which a Timezone object is
320     * constructed.
321     *
322     * @return a Timezone object that contains all GMT offsets and DST
323     * rules information.
324     */
325    Timezone phase2(String zoneName) {
326        Timezone tz = new Timezone(zoneName);
327        Zone zone = getZone(zoneName);
328        zone.resolve(this);
329
330        // TODO: merge phase2's for the regular and SimpleTimeZone ones.
331        if (isYearForTimeZoneDataSpecified) {
332            ZoneRec zrec = zone.get(zone.size()-1);
333            tz.setLastZoneRec(zrec);
334            tz.setRawOffset(zrec.getGmtOffset());
335            if (zrec.hasRuleReference()) {
336                /*
337                 * This part assumes that the specified year is covered by
338                 * the rules referred to by the last zone record.
339                 */
340                List<RuleRec> rrecs = zrec.getRuleRef().getRules(startYear);
341
342                if (rrecs.size() == 2) {
343                    // make sure that one is a start rule and the other is
344                    // an end rule.
345                    RuleRec r0 = rrecs.get(0);
346                    RuleRec r1 = rrecs.get(1);
347                    if (r0.getSave() == 0 && r1.getSave() > 0) {
348                        rrecs.set(0, r1);
349                        rrecs.set(1, r0);
350                    } else if (!(r0.getSave() > 0 && r1.getSave() == 0)) {
351                        rrecs = null;
352                        Main.error(zoneName + ": rules for " +  startYear + " not found.");
353                    }
354                } else {
355                    rrecs = null;
356                }
357                if (rrecs != null) {
358                    tz.setLastRules(rrecs);
359                }
360            }
361            return tz;
362        }
363
364        int gmtOffset;
365        int year = minYear;
366        int fromYear = year;
367        long fromTime = Time.getLocalTime(startYear,
368                                          Month.JANUARY,
369                                          1, 0);
370
371        // take the index 0 for the GMT offset of the last zone record
372        ZoneRec zrec = zone.get(zone.size()-1);
373        tz.getOffsetIndex(zrec.getGmtOffset());
374
375        int lastGmtOffsetValue = -1;
376        ZoneRec prevzrec = null;
377        int currentSave = 0;
378        boolean usedZone;
379        for (int zindex = 0; zindex < zone.size(); zindex++) {
380            zrec = zone.get(zindex);
381            usedZone = false;
382            gmtOffset = zrec.getGmtOffset();
383            int stdOffset = zrec.getDirectSave();
384
385            if (gmtOffset != lastGmtOffsetValue) {
386                tz.setRawOffset(gmtOffset, fromTime);
387                lastGmtOffsetValue = gmtOffset;
388            }
389            // If this is the last zone record, take the last rule info.
390            if (!zrec.hasUntil()) {
391                if (zrec.hasRuleReference()) {
392                    tz.setLastRules(zrec.getRuleRef().getLastRules());
393                } else if (stdOffset != 0) {
394                    // in case the last rule is all year round DST-only
395                    // (Asia/Amman once announced this rule.)
396                    tz.setLastDSTSaving(stdOffset);
397                }
398            }
399            if (!zrec.hasRuleReference()) {
400                if (!zrec.hasUntil() || zrec.getUntilTime(stdOffset) >= fromTime) {
401                    tz.addTransition(fromTime,
402                                     tz.getOffsetIndex(gmtOffset+stdOffset),
403                                     tz.getDstOffsetIndex(stdOffset));
404                    usedZone = true;
405                }
406                currentSave = stdOffset;
407                // optimization in case the last rule is fixed.
408                if (!zrec.hasUntil()) {
409                    if (tz.getNTransitions() > 0) {
410                        if (stdOffset == 0) {
411                            tz.setDSTType(Timezone.X_DST);
412                        } else {
413                            tz.setDSTType(Timezone.LAST_DST);
414                        }
415                        long time = Time.getLocalTime(maxYear,
416                                                      Month.JANUARY, 1, 0);
417                        time -= zrec.getGmtOffset();
418                        tz.addTransition(time,
419                                         tz.getOffsetIndex(gmtOffset+stdOffset),
420                                         tz.getDstOffsetIndex(stdOffset));
421                        tz.addUsedRec(zrec);
422                    } else {
423                        tz.setDSTType(Timezone.NO_DST);
424                    }
425                    break;
426                }
427            } else {
428                Rule rule = zrec.getRuleRef();
429                boolean fromTimeUsed = false;
430                currentSave = 0;
431            year_loop:
432                for (year = getMinYear(); year <= endYear; year++) {
433                    if (zrec.hasUntil() && year > zrec.getUntilYear()) {
434                        break;
435                    }
436                    List<RuleRec> rules = rule.getRules(year);
437                    if (rules.size() > 0) {
438                        for (int i = 0; i < rules.size(); i++) {
439                            RuleRec rrec = rules.get(i);
440                            long transition = rrec.getTransitionTime(year,
441                                                                     gmtOffset,
442                                                                     currentSave);
443                            if (zrec.hasUntil()) {
444                                if (transition >= zrec.getUntilTime(currentSave)) {
445                                    // If the GMT offset changed from the previous one,
446                                    // record fromTime as a transition.
447                                    if (!fromTimeUsed && prevzrec != null
448                                        && gmtOffset != prevzrec.getGmtOffset()) {
449                                        tz.addTransition(fromTime,
450                                                         tz.getOffsetIndex(gmtOffset+currentSave),
451                                                         tz.getDstOffsetIndex(currentSave));
452                                        fromTimeUsed = true; // for consistency
453                                    }
454                                    break year_loop;
455                                }
456                            }
457
458                            if (fromTimeUsed == false) {
459                                if (fromTime <= transition) {
460                                    fromTimeUsed = true;
461
462                                    if (fromTime != minTime) {
463                                        int prevsave;
464
465                                        // See if until time in the previous
466                                        // ZoneRec is the same thing as the
467                                        // local time in the next rule.
468                                        // (examples are Asia/Ashkhabad in 1991,
469                                        // Europe/Riga in 1989)
470
471                                        if (i > 0) {
472                                            prevsave = rules.get(i-1).getSave();
473                                        } else {
474                                            List<RuleRec> prevrules = rule.getRules(year-1);
475
476                                            if (prevrules.size() > 0) {
477                                                prevsave = prevrules.get(prevrules.size()-1).getSave();
478                                            } else {
479                                                prevsave = 0;
480                                            }
481                                        }
482
483                                        if (rrec.isSameTransition(prevzrec, prevsave, gmtOffset)) {
484                                            currentSave = rrec.getSave();
485                                            tz.addTransition(fromTime,
486                                                         tz.getOffsetIndex(gmtOffset+currentSave),
487                                                         tz.getDstOffsetIndex(currentSave));
488                                            tz.addUsedRec(rrec);
489                                            usedZone = true;
490                                            continue;
491                                        }
492                                        if (!prevzrec.hasRuleReference()
493                                            || rule != prevzrec.getRuleRef()
494                                            || (rule == prevzrec.getRuleRef()
495                                                && gmtOffset != prevzrec.getGmtOffset())) {
496                                            int save = (fromTime == transition) ? rrec.getSave() : currentSave;
497                                            tz.addTransition(fromTime,
498                                                         tz.getOffsetIndex(gmtOffset+save),
499                                                         tz.getDstOffsetIndex(save));
500                                            tz.addUsedRec(rrec);
501                                            usedZone = true;
502                                        }
503                                    } else {  // fromTime == minTime
504                                        int save = rrec.getSave();
505                                        tz.addTransition(minTime,
506                                                         tz.getOffsetIndex(gmtOffset),
507                                                         tz.getDstOffsetIndex(0));
508
509                                        tz.addTransition(transition,
510                                                         tz.getOffsetIndex(gmtOffset+save),
511                                                         tz.getDstOffsetIndex(save));
512
513                                        tz.addUsedRec(rrec);
514                                        usedZone = true;
515                                    }
516                                } else if (year == fromYear && i == rules.size()-1) {
517                                    int save = rrec.getSave();
518                                    tz.addTransition(fromTime,
519                                                     tz.getOffsetIndex(gmtOffset+save),
520                                                     tz.getDstOffsetIndex(save));
521                                }
522                            }
523
524                            currentSave = rrec.getSave();
525                            if (fromTime < transition) {
526                                tz.addTransition(transition,
527                                                 tz.getOffsetIndex(gmtOffset+currentSave),
528                                                 tz.getDstOffsetIndex(currentSave));
529                                tz.addUsedRec(rrec);
530                                usedZone = true;
531                            }
532                        }
533                    } else {
534                        if (year == fromYear) {
535                            tz.addTransition(fromTime,
536                                             tz.getOffsetIndex(gmtOffset+currentSave),
537                                             tz.getDstOffsetIndex(currentSave));
538                            fromTimeUsed = true;
539                        }
540                        if (year == endYear && !zrec.hasUntil()) {
541                            if (tz.getNTransitions() > 0) {
542                                // Assume that this Zone stopped DST
543                                tz.setDSTType(Timezone.X_DST);
544                                long time = Time.getLocalTime(maxYear, Month.JANUARY,
545                                                              1, 0);
546                                time -= zrec.getGmtOffset();
547                                tz.addTransition(time,
548                                                 tz.getOffsetIndex(gmtOffset),
549                                                 tz.getDstOffsetIndex(0));
550                                usedZone = true;
551                            } else {
552                                tz.setDSTType(Timezone.NO_DST);
553                            }
554                        }
555                    }
556                }
557            }
558            if (usedZone) {
559                tz.addUsedRec(zrec);
560            }
561            if (zrec.hasUntil() && zrec.getUntilTime(currentSave) > fromTime) {
562                fromTime = zrec.getUntilTime(currentSave);
563                fromYear = zrec.getUntilYear();
564                year = zrec.getUntilYear();
565            }
566            prevzrec = zrec;
567        }
568
569        if (tz.getDSTType() == Timezone.UNDEF_DST) {
570            tz.setDSTType(Timezone.DST);
571        }
572        tz.optimize();
573        tz.checksum();
574        return tz;
575    }
576
577    private static void panic(String msg) {
578        Main.panic(msg);
579    }
580}
581