• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
3  *
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are met:
8  *
9  *  * Redistributions of source code must retain the above copyright notice,
10  *    this list of conditions and the following disclaimer.
11  *
12  *  * Redistributions in binary form must reproduce the above copyright notice,
13  *    this list of conditions and the following disclaimer in the documentation
14  *    and/or other materials provided with the distribution.
15  *
16  *  * Neither the name of JSR-310 nor the names of its contributors
17  *    may be used to endorse or promote products derived from this software
18  *    without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  */
32 package org.threeten.bp.zone;
33 
34 import static org.threeten.bp.temporal.ChronoField.HOUR_OF_DAY;
35 import static org.threeten.bp.temporal.ChronoField.MINUTE_OF_HOUR;
36 import static org.threeten.bp.temporal.ChronoField.SECOND_OF_MINUTE;
37 
38 import java.io.BufferedReader;
39 import java.io.ByteArrayOutputStream;
40 import java.io.DataOutputStream;
41 import java.io.File;
42 import java.io.FileOutputStream;
43 import java.io.FileReader;
44 import java.io.IOException;
45 import java.io.OutputStream;
46 import java.text.ParsePosition;
47 import java.util.ArrayList;
48 import java.util.Arrays;
49 import java.util.HashMap;
50 import java.util.HashSet;
51 import java.util.List;
52 import java.util.Map;
53 import java.util.Set;
54 import java.util.SortedMap;
55 import java.util.StringTokenizer;
56 import java.util.TreeMap;
57 import java.util.TreeSet;
58 import java.util.jar.JarOutputStream;
59 import java.util.zip.ZipEntry;
60 
61 import org.threeten.bp.DayOfWeek;
62 import org.threeten.bp.LocalDate;
63 import org.threeten.bp.LocalDateTime;
64 import org.threeten.bp.LocalTime;
65 import org.threeten.bp.Month;
66 import org.threeten.bp.Year;
67 import org.threeten.bp.ZoneOffset;
68 import org.threeten.bp.format.DateTimeFormatter;
69 import org.threeten.bp.format.DateTimeFormatterBuilder;
70 import org.threeten.bp.jdk8.Jdk8Methods;
71 import org.threeten.bp.temporal.TemporalAccessor;
72 import org.threeten.bp.temporal.TemporalAdjusters;
73 import org.threeten.bp.zone.ZoneOffsetTransitionRule.TimeDefinition;
74 
75 /**
76  * A builder that can read the TZDB time-zone files and build {@code ZoneRules} instances.
77  *
78  * <h3>Specification for implementors</h3>
79  * This class is a mutable builder. A new instance must be created for each compile.
80  */
81 final class TzdbZoneRulesCompiler {
82 
83     /**
84      * Time parser.
85      */
86     private static final DateTimeFormatter TIME_PARSER;
87     static {
88         TIME_PARSER = new DateTimeFormatterBuilder()
89             .appendValue(HOUR_OF_DAY)
90             .optionalStart().appendLiteral(':').appendValue(MINUTE_OF_HOUR, 2)
91             .optionalStart().appendLiteral(':').appendValue(SECOND_OF_MINUTE, 2)
92             .toFormatter();
93     }
94 
95     /**
96      * Reads a set of TZDB files and builds a single combined data file.
97      *
98      * @param args  the arguments
99      */
main(String[] args)100     public static void main(String[] args) {
101         if (args.length < 2) {
102             outputHelp();
103             return;
104         }
105 
106         // parse args
107         String version = null;
108         File baseSrcDir = null;
109         File dstDir = null;
110         boolean unpacked = false;
111         boolean verbose = false;
112 
113         // parse options
114         int i;
115         for (i = 0; i < args.length; i++) {
116             String arg = args[i];
117             if (arg.startsWith("-") == false) {
118                 break;
119             }
120             if ("-srcdir".equals(arg)) {
121                 if (baseSrcDir == null && ++i < args.length) {
122                     baseSrcDir = new File(args[i]);
123                     continue;
124                 }
125             } else if ("-dstdir".equals(arg)) {
126                 if (dstDir == null && ++i < args.length) {
127                     dstDir = new File(args[i]);
128                     continue;
129                 }
130             } else if ("-version".equals(arg)) {
131                 if (version == null && ++i < args.length) {
132                     version = args[i];
133                     continue;
134                 }
135             } else if ("-unpacked".equals(arg)) {
136                 if (unpacked == false) {
137                     unpacked = true;
138                     continue;
139                 }
140             } else if ("-verbose".equals(arg)) {
141                 if (verbose == false) {
142                     verbose = true;
143                     continue;
144                 }
145             } else if ("-help".equals(arg) == false) {
146                 System.out.println("Unrecognised option: " + arg);
147             }
148             outputHelp();
149             return;
150         }
151 
152         // check source directory
153         if (baseSrcDir == null) {
154             System.out.println("Source directory must be specified using -srcdir: " + baseSrcDir);
155             return;
156         }
157         if (baseSrcDir.isDirectory() == false) {
158             System.out.println("Source does not exist or is not a directory: " + baseSrcDir);
159             return;
160         }
161         dstDir = (dstDir != null ? dstDir : baseSrcDir);
162 
163         // parse source file names
164         List<String> srcFileNames = Arrays.asList(Arrays.copyOfRange(args, i, args.length));
165         if (srcFileNames.isEmpty()) {
166             System.out.println("Source filenames not specified, using default set");
167             System.out.println("(africa antarctica asia australasia backward etcetera europe northamerica southamerica)");
168             srcFileNames = Arrays.asList("africa", "antarctica", "asia", "australasia", "backward",
169                     "etcetera", "europe", "northamerica", "southamerica");
170         }
171 
172         // find source directories to process
173         List<File> srcDirs = new ArrayList<File>();
174         if (version != null) {
175             File srcDir = new File(baseSrcDir, version);
176             if (srcDir.isDirectory() == false) {
177                 System.out.println("Version does not represent a valid source directory : " + srcDir);
178                 return;
179             }
180             srcDirs.add(srcDir);
181         } else {
182             File[] dirs = baseSrcDir.listFiles();
183             for (File dir : dirs) {
184                 if (dir.isDirectory() && dir.getName().matches("[12][0-9][0-9][0-9][A-Za-z0-9._-]+")) {
185                     srcDirs.add(dir);
186                 }
187             }
188         }
189         if (srcDirs.isEmpty()) {
190             System.out.println("Source directory contains no valid source folders: " + baseSrcDir);
191             return;
192         }
193 
194         // check destination directory
195         if (dstDir.exists() == false && dstDir.mkdirs() == false) {
196             System.out.println("Destination directory could not be created: " + dstDir);
197             return;
198         }
199         if (dstDir.isDirectory() == false) {
200             System.out.println("Destination is not a directory: " + dstDir);
201             return;
202         }
203         process(srcDirs, srcFileNames, dstDir, unpacked, verbose);
204     }
205 
206     /**
207      * Output usage text for the command line.
208      */
outputHelp()209     private static void outputHelp() {
210         System.out.println("Usage: TzdbZoneRulesCompiler <options> <tzdb source filenames>");
211         System.out.println("where options include:");
212         System.out.println("   -srcdir <directory>   Where to find source directories (required)");
213         System.out.println("   -dstdir <directory>   Where to output generated files (default srcdir)");
214         System.out.println("   -version <version>    Specify the version, such as 2009a (optional)");
215         System.out.println("   -unpacked             Generate dat files without jar files");
216         System.out.println("   -help                 Print this usage message");
217         System.out.println("   -verbose              Output verbose information during compilation");
218         System.out.println(" There must be one directory for each version in srcdir");
219         System.out.println(" Each directory must have the name of the version, such as 2009a");
220         System.out.println(" Each directory must contain the unpacked tzdb files, such as asia or europe");
221         System.out.println(" Directories must match the regex [12][0-9][0-9][0-9][A-Za-z0-9._-]+");
222         System.out.println(" There will be one jar file for each version and one combined jar in dstdir");
223         System.out.println(" If the version is specified, only that version is processed");
224     }
225 
226     /**
227      * Process to create the jar files.
228      */
process(List<File> srcDirs, List<String> srcFileNames, File dstDir, boolean unpacked, boolean verbose)229     private static void process(List<File> srcDirs, List<String> srcFileNames, File dstDir, boolean unpacked, boolean verbose) {
230         // build actual jar files
231         Map<Object, Object> deduplicateMap = new HashMap<Object, Object>();
232         Map<String, SortedMap<String, ZoneRules>> allBuiltZones = new TreeMap<String, SortedMap<String, ZoneRules>>();
233         Set<String> allRegionIds = new TreeSet<String>();
234         Set<ZoneRules> allRules = new HashSet<ZoneRules>();
235         SortedMap<LocalDate, Byte> bestLeapSeconds = null;
236 
237         for (File srcDir : srcDirs) {
238             // source files in this directory
239             List<File> srcFiles = new ArrayList<File>();
240             for (String srcFileName : srcFileNames) {
241                 File file = new File(srcDir, srcFileName);
242                 if (file.exists()) {
243                     srcFiles.add(file);
244                 }
245             }
246             if (srcFiles.isEmpty()) {
247                 continue;  // nothing to process
248             }
249             File leapSecondsFile = new File(srcDir, "leapseconds");
250             if (!leapSecondsFile.exists()) {
251                 System.out.println("Version " + srcDir.getName() + " does not include leap seconds information.");
252                 leapSecondsFile = null;
253             }
254 
255             // compile
256             String loopVersion = srcDir.getName();
257             TzdbZoneRulesCompiler compiler = new TzdbZoneRulesCompiler(loopVersion, srcFiles, leapSecondsFile, verbose);
258             compiler.setDeduplicateMap(deduplicateMap);
259             try {
260                 // compile
261                 compiler.compile();
262                 SortedMap<String, ZoneRules> builtZones = compiler.getZones();
263                 SortedMap<LocalDate, Byte> parsedLeapSeconds = compiler.getLeapSeconds();
264 
265                 // output version-specific file
266                 if (unpacked == false) {
267                     File dstFile = new File(dstDir, "threeten-TZDB-" + loopVersion + ".jar");
268                     if (verbose) {
269                         System.out.println("Outputting file: " + dstFile);
270                     }
271                     outputFile(dstFile, loopVersion, builtZones, parsedLeapSeconds);
272                 }
273 
274                 // create totals
275                 allBuiltZones.put(loopVersion, builtZones);
276                 allRegionIds.addAll(builtZones.keySet());
277                 allRules.addAll(builtZones.values());
278 
279                 // track best possible leap seconds collection
280                 if (compiler.getMostRecentLeapSecond() != null) {
281                     // we've got a live one!
282                     if (bestLeapSeconds == null || compiler.getMostRecentLeapSecond().compareTo(bestLeapSeconds.lastKey()) > 0) {
283                         // found the first one, or found a better one
284                         bestLeapSeconds = parsedLeapSeconds;
285                     }
286                 }
287             } catch (Exception ex) {
288                 System.out.println("Failed: " + ex.toString());
289                 ex.printStackTrace();
290                 System.exit(1);
291             }
292         }
293 
294         // output merged file
295         if (unpacked) {
296             if (verbose) {
297                 System.out.println("Outputting combined files: " + dstDir);
298             }
299             outputFilesDat(dstDir, allBuiltZones, allRegionIds, allRules, bestLeapSeconds);
300         } else {
301             File dstFile = new File(dstDir, "threeten-TZDB-all.jar");
302             if (verbose) {
303                 System.out.println("Outputting combined file: " + dstFile);
304             }
305             outputFile(dstFile, allBuiltZones, allRegionIds, allRules, bestLeapSeconds);
306         }
307     }
308 
309     /**
310      * Outputs the DAT files.
311      */
outputFilesDat(File dstDir, Map<String, SortedMap<String, ZoneRules>> allBuiltZones, Set<String> allRegionIds, Set<ZoneRules> allRules, SortedMap<LocalDate, Byte> leapSeconds)312     private static void outputFilesDat(File dstDir, Map<String, SortedMap<String, ZoneRules>> allBuiltZones,
313             Set<String> allRegionIds, Set<ZoneRules> allRules, SortedMap<LocalDate, Byte> leapSeconds) {
314         File tzdbFile = new File(dstDir, "TZDB.dat");
315         tzdbFile.delete();
316         try {
317             FileOutputStream fos = null;
318             try {
319                 fos = new FileOutputStream(tzdbFile);
320                 outputTzdbDat(fos, allBuiltZones, allRegionIds, allRules);
321             } finally {
322                 if (fos != null) {
323                     fos.close();
324                 }
325             }
326         } catch (Exception ex) {
327             System.out.println("Failed: " + ex.toString());
328             ex.printStackTrace();
329             System.exit(1);
330         }
331     }
332 
333     /**
334      * Outputs the file.
335      */
outputFile(File dstFile, String version, SortedMap<String, ZoneRules> builtZones, SortedMap<LocalDate, Byte> leapSeconds)336     private static void outputFile(File dstFile, String version, SortedMap<String, ZoneRules> builtZones, SortedMap<LocalDate, Byte> leapSeconds) {
337         Map<String, SortedMap<String, ZoneRules>> loopAllBuiltZones = new TreeMap<String, SortedMap<String, ZoneRules>>();
338         loopAllBuiltZones.put(version, builtZones);
339         Set<String> loopAllRegionIds = new TreeSet<String>(builtZones.keySet());
340         Set<ZoneRules> loopAllRules = new HashSet<ZoneRules>(builtZones.values());
341         outputFile(dstFile, loopAllBuiltZones, loopAllRegionIds, loopAllRules, leapSeconds);
342     }
343 
344     /**
345      * Outputs the file.
346      */
outputFile(File dstFile, Map<String, SortedMap<String, ZoneRules>> allBuiltZones, Set<String> allRegionIds, Set<ZoneRules> allRules, SortedMap<LocalDate, Byte> leapSeconds)347     private static void outputFile(File dstFile, Map<String, SortedMap<String, ZoneRules>> allBuiltZones,
348             Set<String> allRegionIds, Set<ZoneRules> allRules, SortedMap<LocalDate, Byte> leapSeconds) {
349         JarOutputStream jos = null;
350         try {
351             jos = new JarOutputStream(new FileOutputStream(dstFile));
352             outputTzdbEntry(jos, allBuiltZones, allRegionIds, allRules);
353         } catch (Exception ex) {
354             System.out.println("Failed: " + ex.toString());
355             ex.printStackTrace();
356             System.exit(1);
357         } finally {
358             if (jos != null) {
359                 try {
360                     jos.close();
361                 } catch (IOException ex) {
362                     // ignore
363                 }
364             }
365         }
366     }
367 
368     /**
369      * Outputs the timezone entry in the JAR file.
370      */
outputTzdbEntry( JarOutputStream jos, Map<String, SortedMap<String, ZoneRules>> allBuiltZones, Set<String> allRegionIds, Set<ZoneRules> allRules)371     private static void outputTzdbEntry(
372             JarOutputStream jos, Map<String, SortedMap<String, ZoneRules>> allBuiltZones,
373             Set<String> allRegionIds, Set<ZoneRules> allRules) {
374         // this format is not publicly specified
375         try {
376             jos.putNextEntry(new ZipEntry("org/threeten/bp/TZDB.dat"));
377             outputTzdbDat(jos, allBuiltZones, allRegionIds, allRules);
378             jos.closeEntry();
379         } catch (Exception ex) {
380             System.out.println("Failed: " + ex.toString());
381             ex.printStackTrace();
382             System.exit(1);
383         }
384     }
385 
386     /**
387      * Outputs the timezone DAT file.
388      */
outputTzdbDat(OutputStream jos, Map<String, SortedMap<String, ZoneRules>> allBuiltZones, Set<String> allRegionIds, Set<ZoneRules> allRules)389     private static void outputTzdbDat(OutputStream jos,
390             Map<String, SortedMap<String, ZoneRules>> allBuiltZones,
391             Set<String> allRegionIds, Set<ZoneRules> allRules) throws IOException {
392         DataOutputStream out = new DataOutputStream(jos);
393 
394         // file version
395         out.writeByte(1);
396         // group
397         out.writeUTF("TZDB");
398         // versions
399         String[] versionArray = allBuiltZones.keySet().toArray(new String[allBuiltZones.size()]);
400         out.writeShort(versionArray.length);
401         for (String version : versionArray) {
402             out.writeUTF(version);
403         }
404         // regions
405         String[] regionArray = allRegionIds.toArray(new String[allRegionIds.size()]);
406         out.writeShort(regionArray.length);
407         for (String regionId : regionArray) {
408             out.writeUTF(regionId);
409         }
410         // rules
411         List<ZoneRules> rulesList = new ArrayList<ZoneRules>(allRules);
412         out.writeShort(rulesList.size());
413         ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
414         for (ZoneRules rules : rulesList) {
415             baos.reset();
416             DataOutputStream dataos = new DataOutputStream(baos);
417             Ser.write(rules, dataos);
418             dataos.close();
419             byte[] bytes = baos.toByteArray();
420             out.writeShort(bytes.length);
421             out.write(bytes);
422         }
423         // link version-region-rules
424         for (String version : allBuiltZones.keySet()) {
425             out.writeShort(allBuiltZones.get(version).size());
426             for (Map.Entry<String, ZoneRules> entry : allBuiltZones.get(version).entrySet()) {
427                  int regionIndex = Arrays.binarySearch(regionArray, entry.getKey());
428                  int rulesIndex = rulesList.indexOf(entry.getValue());
429                  out.writeShort(regionIndex);
430                  out.writeShort(rulesIndex);
431             }
432         }
433         out.flush();
434     }
435 
436     //-----------------------------------------------------------------------
437     /** The TZDB rules. */
438     private final Map<String, List<TZDBRule>> rules = new HashMap<String, List<TZDBRule>>();
439     /** The TZDB zones. */
440     private final Map<String, List<TZDBZone>> zones = new HashMap<String, List<TZDBZone>>();
441     /** The TZDB links. */
442     private final Map<String, String> links = new HashMap<String, String>();
443     /** The built zones. */
444     private final SortedMap<String, ZoneRules> builtZones = new TreeMap<String, ZoneRules>();
445     /** A map to deduplicate object instances. */
446     private Map<Object, Object> deduplicateMap = new HashMap<Object, Object>();
447     /** Sorted collection of LeapSecondRules. */
448     private final SortedMap<LocalDate, Byte> leapSeconds = new TreeMap<LocalDate, Byte>();
449 
450     /** The version to produce. */
451     private final String version;
452     /** The source files. */
453     private final List<File> sourceFiles;
454     /** The leap seconds file. */
455     private final File leapSecondsFile;
456     /** The version to produce. */
457     private final boolean verbose;
458 
459     /**
460      * Creates an instance if you want to invoke the compiler manually.
461      *
462      * @param version  the version, such as 2009a, not null
463      * @param sourceFiles  the list of source files, not empty, not null
464      * @param verbose  whether to output verbose messages
465      */
TzdbZoneRulesCompiler(String version, List<File> sourceFiles, File leapSecondsFile, boolean verbose)466     public TzdbZoneRulesCompiler(String version, List<File> sourceFiles, File leapSecondsFile, boolean verbose) {
467         this.version = version;
468         this.sourceFiles = sourceFiles;
469         this.leapSecondsFile = leapSecondsFile;
470         this.verbose = verbose;
471     }
472 
473     /**
474      * Compile the rules file.
475      * <p>
476      * Use {@link #getZones()} and {@link #getLeapSeconds()} to retrieve the parsed data.
477      *
478      * @throws Exception if an error occurs
479      */
compile()480     public void compile() throws Exception {
481         printVerbose("Compiling TZDB version " + version);
482         parseFiles();
483         parseLeapSecondsFile();
484         buildZoneRules();
485         printVerbose("Compiled TZDB version " + version);
486     }
487 
488     /**
489      * Gets the parsed zone rules.
490      *
491      * @return the parsed zone rules, not null
492      */
getZones()493     public SortedMap<String, ZoneRules> getZones() {
494         return builtZones;
495     }
496 
497     /**
498      * Gets the parsed leap seconds.
499      *
500      * @return the parsed and sorted leap seconds, not null
501      */
getLeapSeconds()502     public SortedMap<LocalDate, Byte> getLeapSeconds() {
503         return leapSeconds;
504     }
505 
506     /**
507      * Gets the most recent leap second.
508      *
509      * @return the most recent leap second, null if none
510      */
getMostRecentLeapSecond()511     private LocalDate getMostRecentLeapSecond() {
512         return leapSeconds.isEmpty() ? null : leapSeconds.lastKey();
513     }
514 
515     /**
516      * Sets the deduplication map.
517      *
518      * @param deduplicateMap  the map to deduplicate items
519      */
setDeduplicateMap(Map<Object, Object> deduplicateMap)520     void setDeduplicateMap(Map<Object, Object> deduplicateMap) {
521         this.deduplicateMap = deduplicateMap;
522     }
523 
524     //-----------------------------------------------------------------------
525     /**
526      * Parses the source files.
527      *
528      * @throws Exception if an error occurs
529      */
parseFiles()530     private void parseFiles() throws Exception {
531         for (File file : sourceFiles) {
532             printVerbose("Parsing file: " + file);
533             parseFile(file);
534         }
535     }
536 
537     /**
538      * Parses the leap seconds file.
539      *
540      * @throws Exception if an error occurs
541      */
parseLeapSecondsFile()542     private void parseLeapSecondsFile() throws Exception {
543         printVerbose("Parsing leap second file: " + leapSecondsFile);
544         int lineNumber = 1;
545         String line = null;
546         BufferedReader in = null;
547 
548         try {
549             in = new BufferedReader(new FileReader(leapSecondsFile));
550             for ( ; (line = in.readLine()) != null; lineNumber++) {
551                 int index = line.indexOf('#');  // remove comments (doesn't handle # in quotes)
552                 if (index >= 0) {
553                     line = line.substring(0, index);
554                 }
555                 if (line.trim().length() == 0) {  // ignore blank lines
556                     continue;
557                 }
558                 LeapSecondRule secondRule = parseLeapSecondRule(line);
559                 leapSeconds.put(secondRule.leapDate, secondRule.secondAdjustment);
560             }
561         } catch (Exception ex) {
562             throw new Exception("Failed while processing file '" + leapSecondsFile + "' on line " + lineNumber + " '" + line + "'", ex);
563         } finally {
564             try {
565                 if (in != null) {
566                     in.close();
567                 }
568             } catch (Exception ex) {
569                 // ignore NPE and IOE
570             }
571         }
572     }
573 
parseLeapSecondRule(String line)574     private LeapSecondRule parseLeapSecondRule(String line) {
575         //    # Leap    YEAR    MONTH    DAY    HH:MM:SS    CORR    R/S
576         //    Leap    1972    Jun    30    23:59:60    +    S
577         //    Leap    1972    Dec    31    23:59:60    +    S
578         //    Leap    1973    Dec    31    23:59:60    +    S
579         //    Leap    1974    Dec    31    23:59:60    +    S
580         //    Leap    1975    Dec    31    23:59:60    +    S
581         //    Leap    1976    Dec    31    23:59:60    +    S
582         //    Leap    1977    Dec    31    23:59:60    +    S
583         //    Leap    1978    Dec    31    23:59:60    +    S
584         //    Leap    1979    Dec    31    23:59:60    +    S
585         //    Leap    1981    Jun    30    23:59:60    +    S
586         //    Leap    1982    Jun    30    23:59:60    +    S
587         //    Leap    1983    Jun    30    23:59:60    +    S
588 
589         StringTokenizer st = new StringTokenizer(line, " \t");
590         String first = st.nextToken();
591         if (first.equals("Leap")) {
592             if (st.countTokens() < 6) {
593                 printVerbose("Invalid leap second line in file: " + leapSecondsFile + ", line: " + line);
594                 throw new IllegalArgumentException("Invalid leap second line");
595             }
596         } else {
597             throw new IllegalArgumentException("Unknown line");
598         }
599 
600         int year = Integer.parseInt(st.nextToken());
601         Month month = parseMonth(st.nextToken());
602         int dayOfMonth = Integer.parseInt(st.nextToken());
603         LocalDate leapDate = LocalDate.of(year, month, dayOfMonth);
604         String timeOfLeapSecond = st.nextToken();
605 
606         byte adjustmentByte = 0;
607         String adjustment = st.nextToken();
608         if (adjustment.equals("+")) {
609             if (!("23:59:60".equals(timeOfLeapSecond))) {
610                 throw new IllegalArgumentException("Leap seconds can only be inserted at 23:59:60 - Date:" + leapDate);
611             }
612             adjustmentByte = +1;
613         } else if (adjustment.equals("-")) {
614             if (!("23:59:59".equals(timeOfLeapSecond))) {
615                 throw new IllegalArgumentException("Leap seconds can only be removed at 23:59:59 - Date:" + leapDate);
616             }
617             adjustmentByte = -1;
618         } else {
619             throw new IllegalArgumentException("Invalid adjustment '" + adjustment + "' in leap second rule for " + leapDate);
620         }
621 
622         String rollingOrStationary = st.nextToken();
623         if (!"S".equalsIgnoreCase(rollingOrStationary)) {
624             throw new IllegalArgumentException("Only stationary ('S') leap seconds are supported, not '" + rollingOrStationary + "'");
625         }
626         return new LeapSecondRule(leapDate, adjustmentByte);
627     }
628 
629     /**
630      * Parses a source file.
631      *
632      * @param file  the file being read, not null
633      * @throws Exception if an error occurs
634      */
parseFile(File file)635     private void parseFile(File file) throws Exception {
636         int lineNumber = 1;
637         String line = null;
638         BufferedReader in = null;
639         try {
640             in = new BufferedReader(new FileReader(file));
641             List<TZDBZone> openZone = null;
642             for ( ; (line = in.readLine()) != null; lineNumber++) {
643                 int index = line.indexOf('#');  // remove comments (doesn't handle # in quotes)
644                 if (index >= 0) {
645                     line = line.substring(0, index);
646                 }
647                 if (line.trim().length() == 0) {  // ignore blank lines
648                     continue;
649                 }
650                 StringTokenizer st = new StringTokenizer(line, " \t");
651                 if (openZone != null && Character.isWhitespace(line.charAt(0)) && st.hasMoreTokens()) {
652                     if (parseZoneLine(st, openZone)) {
653                         openZone = null;
654                     }
655                 } else {
656                     if (st.hasMoreTokens()) {
657                         String first = st.nextToken();
658                         if (first.equals("Zone")) {
659                             if (st.countTokens() < 3) {
660                                 printVerbose("Invalid Zone line in file: " + file + ", line: " + line);
661                                 throw new IllegalArgumentException("Invalid Zone line");
662                             }
663                             openZone = new ArrayList<TZDBZone>();
664                             zones.put(st.nextToken(), openZone);
665                             if (parseZoneLine(st, openZone)) {
666                                 openZone = null;
667                             }
668                         } else {
669                             openZone = null;
670                             if (first.equals("Rule")) {
671                                 if (st.countTokens() < 9) {
672                                     printVerbose("Invalid Rule line in file: " + file + ", line: " + line);
673                                     throw new IllegalArgumentException("Invalid Rule line");
674                                 }
675                                 parseRuleLine(st);
676 
677                             } else if (first.equals("Link")) {
678                                 if (st.countTokens() < 2) {
679                                     printVerbose("Invalid Link line in file: " + file + ", line: " + line);
680                                     throw new IllegalArgumentException("Invalid Link line");
681                                 }
682                                 String realId = st.nextToken();
683                                 String aliasId = st.nextToken();
684                                 links.put(aliasId, realId);
685 
686                             } else {
687                                 throw new IllegalArgumentException("Unknown line");
688                             }
689                         }
690                     }
691                 }
692             }
693         } catch (Exception ex) {
694             throw new Exception("Failed while processing file '" + file + "' on line " + lineNumber + " '" + line + "'", ex);
695         } finally {
696             if (in != null) {
697                 in.close();
698             }
699         }
700     }
701 
702     /**
703      * Parses a Rule line.
704      *
705      * @param st  the tokenizer, not null
706      */
parseRuleLine(StringTokenizer st)707     private void parseRuleLine(StringTokenizer st) {
708         TZDBRule rule = new TZDBRule();
709         String name = st.nextToken();
710         if (rules.containsKey(name) == false) {
711             rules.put(name, new ArrayList<TZDBRule>());
712         }
713         rules.get(name).add(rule);
714         rule.startYear = parseYear(st.nextToken(), 0);
715         rule.endYear = parseYear(st.nextToken(), rule.startYear);
716         if (rule.startYear > rule.endYear) {
717             throw new IllegalArgumentException("Year order invalid: " + rule.startYear + " > " + rule.endYear);
718         }
719         parseOptional(st.nextToken());  // type is unused
720         parseMonthDayTime(st, rule);
721         rule.savingsAmount = parsePeriod(st.nextToken());
722         rule.text = parseOptional(st.nextToken());
723     }
724 
725     /**
726      * Parses a Zone line.
727      *
728      * @param st  the tokenizer, not null
729      * @return true if the zone is complete
730      */
parseZoneLine(StringTokenizer st, List<TZDBZone> zoneList)731     private boolean parseZoneLine(StringTokenizer st, List<TZDBZone> zoneList) {
732         TZDBZone zone = new TZDBZone();
733         zoneList.add(zone);
734         zone.standardOffset = parseOffset(st.nextToken());
735         String savingsRule = parseOptional(st.nextToken());
736         if (savingsRule == null) {
737             zone.fixedSavingsSecs = 0;
738             zone.savingsRule = null;
739         } else {
740             try {
741                 zone.fixedSavingsSecs = parsePeriod(savingsRule);
742                 zone.savingsRule = null;
743             } catch (Exception ex) {
744                 zone.fixedSavingsSecs = null;
745                 zone.savingsRule = savingsRule;
746             }
747         }
748         zone.text = st.nextToken();
749         if (st.hasMoreTokens()) {
750             zone.year = Year.of(Integer.parseInt(st.nextToken()));
751             if (st.hasMoreTokens()) {
752                 parseMonthDayTime(st, zone);
753             }
754             return false;
755         } else {
756             return true;
757         }
758     }
759 
760     /**
761      * Parses a Rule line.
762      *
763      * @param st  the tokenizer, not null
764      * @param mdt  the object to parse into, not null
765      */
parseMonthDayTime(StringTokenizer st, TZDBMonthDayTime mdt)766     private void parseMonthDayTime(StringTokenizer st, TZDBMonthDayTime mdt) {
767         mdt.month = parseMonth(st.nextToken());
768         if (st.hasMoreTokens()) {
769             String dayRule = st.nextToken();
770             if (dayRule.startsWith("last")) {
771                 mdt.dayOfMonth = -1;
772                 mdt.dayOfWeek = parseDayOfWeek(dayRule.substring(4));
773                 mdt.adjustForwards = false;
774             } else {
775                 int index = dayRule.indexOf(">=");
776                 if (index > 0) {
777                     mdt.dayOfWeek = parseDayOfWeek(dayRule.substring(0, index));
778                     dayRule = dayRule.substring(index + 2);
779                 } else {
780                     index = dayRule.indexOf("<=");
781                     if (index > 0) {
782                         mdt.dayOfWeek = parseDayOfWeek(dayRule.substring(0, index));
783                         mdt.adjustForwards = false;
784                         dayRule = dayRule.substring(index + 2);
785                     }
786                 }
787                 mdt.dayOfMonth = Integer.parseInt(dayRule);
788             }
789             if (st.hasMoreTokens()) {
790                 String timeStr = st.nextToken();
791                 int timeOfDaySecs = parseSecs(timeStr);
792                 LocalTime time = deduplicate(LocalTime.ofSecondOfDay(Jdk8Methods.floorMod(timeOfDaySecs, 86400)));
793                 mdt.time = time;
794                 mdt.adjustDays = Jdk8Methods.floorDiv(timeOfDaySecs, 86400);
795                 mdt.timeDefinition = parseTimeDefinition(timeStr.charAt(timeStr.length() - 1));
796             }
797         }
798     }
799 
parseYear(String str, int defaultYear)800     private int parseYear(String str, int defaultYear) {
801         str = str.toLowerCase();
802         if (matches(str, "minimum")) {
803             return Year.MIN_VALUE;
804         } else if (matches(str, "maximum")) {
805             return Year.MAX_VALUE;
806         } else if (str.equals("only")) {
807             return defaultYear;
808         }
809         return Integer.parseInt(str);
810     }
811 
parseMonth(String str)812     private Month parseMonth(String str) {
813         str = str.toLowerCase();
814         for (Month moy : Month.values()) {
815             if (matches(str, moy.name().toLowerCase())) {
816                 return moy;
817             }
818         }
819         throw new IllegalArgumentException("Unknown month: " + str);
820     }
821 
parseDayOfWeek(String str)822     private DayOfWeek parseDayOfWeek(String str) {
823         str = str.toLowerCase();
824         for (DayOfWeek dow : DayOfWeek.values()) {
825             if (matches(str, dow.name().toLowerCase())) {
826                 return dow;
827             }
828         }
829         throw new IllegalArgumentException("Unknown day-of-week: " + str);
830     }
831 
matches(String str, String search)832     private boolean matches(String str, String search) {
833         return str.startsWith(search.substring(0, 3)) && search.startsWith(str) && str.length() <= search.length();
834     }
835 
parseOptional(String str)836     private String parseOptional(String str) {
837         return str.equals("-") ? null : str;
838     }
839 
parseSecs(String str)840     private int parseSecs(String str) {
841         if (str.equals("-")) {
842             return 0;
843         }
844         int pos = 0;
845         if (str.startsWith("-")) {
846             pos = 1;
847         }
848         ParsePosition pp = new ParsePosition(pos);
849         TemporalAccessor parsed = TIME_PARSER.parseUnresolved(str, pp);
850         if (parsed == null || pp.getErrorIndex() >= 0) {
851             throw new IllegalArgumentException(str);
852         }
853         long hour = parsed.getLong(HOUR_OF_DAY);
854         Long min = (parsed.isSupported(MINUTE_OF_HOUR) ? parsed.getLong(MINUTE_OF_HOUR) : null);
855         Long sec = (parsed.isSupported(SECOND_OF_MINUTE) ? parsed.getLong(SECOND_OF_MINUTE) : null);
856         int secs = (int) (hour * 60 * 60 + (min != null ? min : 0) * 60 + (sec != null ? sec : 0));
857         if (pos == 1) {
858             secs = -secs;
859         }
860         return secs;
861     }
862 
parseOffset(String str)863     private ZoneOffset parseOffset(String str) {
864         int secs = parseSecs(str);
865         return ZoneOffset.ofTotalSeconds(secs);
866     }
867 
parsePeriod(String str)868     private int parsePeriod(String str) {
869         return parseSecs(str);
870     }
871 
parseTimeDefinition(char c)872     private TimeDefinition parseTimeDefinition(char c) {
873         switch (c) {
874             case 's':
875             case 'S':
876                 // standard time
877                 return TimeDefinition.STANDARD;
878             case 'u':
879             case 'U':
880             case 'g':
881             case 'G':
882             case 'z':
883             case 'Z':
884                 // UTC
885                 return TimeDefinition.UTC;
886             case 'w':
887             case 'W':
888             default:
889                 // wall time
890                 return TimeDefinition.WALL;
891         }
892     }
893 
894     //-----------------------------------------------------------------------
895     /**
896      * Build the rules, zones and links into real zones.
897      *
898      * @throws Exception if an error occurs
899      */
buildZoneRules()900     private void buildZoneRules() throws Exception {
901         // build zones
902         for (String zoneId : zones.keySet()) {
903             printVerbose("Building zone " + zoneId);
904             zoneId = deduplicate(zoneId);
905             List<TZDBZone> tzdbZones = zones.get(zoneId);
906             ZoneRulesBuilder bld = new ZoneRulesBuilder();
907             for (TZDBZone tzdbZone : tzdbZones) {
908                 bld = tzdbZone.addToBuilder(bld, rules);
909             }
910             ZoneRules buildRules = bld.toRules(zoneId, deduplicateMap);
911             builtZones.put(zoneId, deduplicate(buildRules));
912         }
913 
914         // build aliases
915         for (String aliasId : links.keySet()) {
916             aliasId = deduplicate(aliasId);
917             String realId = links.get(aliasId);
918             printVerbose("Linking alias " + aliasId + " to " + realId);
919             ZoneRules realRules = builtZones.get(realId);
920             if (realRules == null) {
921                 realId = links.get(realId);  // try again (handle alias liked to alias)
922                 printVerbose("Relinking alias " + aliasId + " to " + realId);
923                 realRules = builtZones.get(realId);
924                 if (realRules == null) {
925                     throw new IllegalArgumentException("Alias '" + aliasId + "' links to invalid zone '" + realId + "' for '" + version + "'");
926                 }
927             }
928             builtZones.put(aliasId, realRules);
929         }
930 
931         // remove UTC and GMT
932         builtZones.remove("UTC");
933         builtZones.remove("GMT");
934         builtZones.remove("GMT0");
935         builtZones.remove("GMT+0");
936         builtZones.remove("GMT-0");
937     }
938 
939     //-----------------------------------------------------------------------
940     /**
941      * Deduplicates an object instance.
942      *
943      * @param <T> the generic type
944      * @param object  the object to deduplicate
945      * @return the deduplicated object
946      */
947     @SuppressWarnings("unchecked")
deduplicate(T object)948     <T> T deduplicate(T object) {
949         if (deduplicateMap.containsKey(object) == false) {
950             deduplicateMap.put(object, object);
951         }
952         return (T) deduplicateMap.get(object);
953     }
954 
955     //-----------------------------------------------------------------------
956     /**
957      * Prints a verbose message.
958      *
959      * @param message  the message, not null
960      */
printVerbose(String message)961     private void printVerbose(String message) {
962         if (verbose) {
963             System.out.println(message);
964         }
965     }
966 
967     //-----------------------------------------------------------------------
968     /**
969      * Class representing a month-day-time in the TZDB file.
970      */
971     abstract class TZDBMonthDayTime {
972         /** The month of the cutover. */
973         Month month = Month.JANUARY;
974         /** The day-of-month of the cutover. */
975         int dayOfMonth = 1;
976         /** Whether to adjust forwards. */
977         boolean adjustForwards = true;
978         /** The day-of-week of the cutover. */
979         DayOfWeek dayOfWeek;
980         /** The time of the cutover. */
981         LocalTime time = LocalTime.MIDNIGHT;
982         /** The time days adjustment. */
983         int adjustDays;
984         /** The time of the cutover. */
985         TimeDefinition timeDefinition = TimeDefinition.WALL;
986 
adjustToFowards(int year)987         void adjustToFowards(int year) {
988             if (adjustForwards == false && dayOfMonth > 0) {
989                 LocalDate adjustedDate = LocalDate.of(year, month, dayOfMonth).minusDays(6);
990                 dayOfMonth = adjustedDate.getDayOfMonth();
991                 month = adjustedDate.getMonth();
992                 adjustForwards = true;
993             }
994         }
995     }
996 
997     //-----------------------------------------------------------------------
998     /**
999      * Class representing a rule line in the TZDB file.
1000      */
1001     final class TZDBRule extends TZDBMonthDayTime {
1002         /** The start year. */
1003         int startYear;
1004         /** The end year. */
1005         int endYear;
1006         /** The amount of savings. */
1007         int savingsAmount;
1008         /** The text name of the zone. */
1009         String text;
1010 
addToBuilder(ZoneRulesBuilder bld)1011         void addToBuilder(ZoneRulesBuilder bld) {
1012             adjustToFowards(2004);  // irrelevant, treat as leap year
1013             bld.addRuleToWindow(startYear, endYear, month, dayOfMonth, dayOfWeek, time, adjustDays, timeDefinition, savingsAmount);
1014         }
1015     }
1016 
1017     //-----------------------------------------------------------------------
1018     /**
1019      * Class representing a linked set of zone lines in the TZDB file.
1020      */
1021     final class TZDBZone extends TZDBMonthDayTime {
1022         /** The standard offset. */
1023         ZoneOffset standardOffset;
1024         /** The fixed savings amount. */
1025         Integer fixedSavingsSecs;
1026         /** The savings rule. */
1027         String savingsRule;
1028         /** The text name of the zone. */
1029         String text;
1030         /** The year of the cutover. */
1031         Year year;
1032 
addToBuilder(ZoneRulesBuilder bld, Map<String, List<TZDBRule>> rules)1033         ZoneRulesBuilder addToBuilder(ZoneRulesBuilder bld, Map<String, List<TZDBRule>> rules) {
1034             if (year != null) {
1035                 bld.addWindow(standardOffset, toDateTime(year.getValue()), timeDefinition);
1036             } else {
1037                 bld.addWindowForever(standardOffset);
1038             }
1039 
1040             if (fixedSavingsSecs != null) {
1041                 bld.setFixedSavingsToWindow(fixedSavingsSecs);
1042             } else {
1043                 List<TZDBRule> tzdbRules = rules.get(savingsRule);
1044                 if (tzdbRules == null) {
1045                     throw new IllegalArgumentException("Rule not found: " + savingsRule);
1046                 }
1047                 for (TZDBRule tzdbRule : tzdbRules) {
1048                     tzdbRule.addToBuilder(bld);
1049                 }
1050             }
1051 
1052             return bld;
1053         }
1054 
toDateTime(int year)1055         private LocalDateTime toDateTime(int year) {
1056             adjustToFowards(year);
1057             LocalDate date;
1058             if (dayOfMonth == -1) {
1059                 dayOfMonth = month.length(Year.isLeap(year));
1060                 date = LocalDate.of(year, month, dayOfMonth);
1061                 if (dayOfWeek != null) {
1062                     date = date.with(TemporalAdjusters.previousOrSame(dayOfWeek));
1063                 }
1064             } else {
1065                 date = LocalDate.of(year, month, dayOfMonth);
1066                 if (dayOfWeek != null) {
1067                     date = date.with(TemporalAdjusters.nextOrSame(dayOfWeek));
1068                 }
1069             }
1070             date = deduplicate(date.plusDays(adjustDays));
1071             return LocalDateTime.of(date, time);
1072         }
1073     }
1074 
1075     //-----------------------------------------------------------------------
1076     /**
1077      * Class representing a rule line in the TZDB file.
1078      */
1079     static final class LeapSecondRule {
1080         /**
1081          * Constructs a rule using fields.
1082          * @param leapDate Date which has gets leap second adjustment (at the end)
1083          * @param secondAdjustment +1 or -1 for inserting or dropping a second
1084          */
LeapSecondRule(LocalDate leapDate, byte secondAdjustment)1085         public LeapSecondRule(LocalDate leapDate, byte secondAdjustment) {
1086             this.leapDate = leapDate;
1087             this.secondAdjustment = secondAdjustment;
1088         }
1089         /** The date of the leap second. */
1090         final LocalDate leapDate;
1091         /** The adjustment (in seconds), +1 means a second is inserted,
1092          * -1 means a second is dropped. */
1093         byte secondAdjustment;
1094     }
1095 
1096 }
1097