1 /*
   2  * Copyright (c) 2012, 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 
  26 /*
  27  * Copyright (c) 2009-2012, Stephen Colebourne & Michael Nascimento Santos
  28  *
  29  * All rights reserved.
  30  *
  31  * Redistribution and use in source and binary forms, with or without
  32  * modification, are permitted provided that the following conditions are met:
  33  *
  34  *  * Redistributions of source code must retain the above copyright notice,
  35  *    this list of conditions and the following disclaimer.
  36  *
  37  *  * Redistributions in binary form must reproduce the above copyright notice,
  38  *    this list of conditions and the following disclaimer in the documentation
  39  *    and/or other materials provided with the distribution.
  40  *
  41  *  * Neither the name of JSR-310 nor the names of its contributors
  42  *    may be used to endorse or promote products derived from this software
  43  *    without specific prior written permission.
  44  *
  45  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  46  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  47  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  48  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  49  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  50  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  51  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  52  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  53  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  54  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  55  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  56  */
  57 package build.tools.tzdb;
  58 
  59 import static build.tools.tzdb.Utils.*;
  60 
  61 import java.io.BufferedReader;
  62 import java.io.ByteArrayOutputStream;
  63 import java.io.DataOutputStream;
  64 import java.io.File;
  65 import java.io.FileOutputStream;
  66 import java.io.FileReader;
  67 import java.text.ParsePosition;
  68 import java.util.ArrayList;
  69 import java.util.Arrays;
  70 import java.util.HashMap;
  71 import java.util.HashSet;
  72 import java.util.List;
  73 import java.util.Map;
  74 import java.util.Set;
  75 import java.util.SortedMap;
  76 import java.util.StringTokenizer;
  77 import java.util.TreeMap;
  78 import java.util.TreeSet;
  79 import java.util.jar.JarOutputStream;
  80 import java.util.zip.ZipEntry;
  81 import java.util.regex.Matcher;
  82 import java.util.regex.Pattern;
  83 
  84 /**
  85  * A builder that can read the TZDB time-zone files and build {@code ZoneRules} instances.
  86  *
  87  * @since 1.8
  88  */
  89 public final class TzdbZoneRulesCompiler {
  90 
  91     private static final Matcher YEAR = Pattern.compile("(?i)(?<min>min)|(?<max>max)|(?<only>only)|(?<year>[0-9]+)").matcher("");
  92     private static final Matcher MONTH = Pattern.compile("(?i)(jan)|(feb)|(mar)|(apr)|(may)|(jun)|(jul)|(aug)|(sep)|(oct)|(nov)|(dec)").matcher("");
  93     private static final Matcher DOW = Pattern.compile("(?i)(mon)|(tue)|(wed)|(thu)|(fri)|(sat)|(sun)").matcher("");
  94     private static final Matcher TIME = Pattern.compile("(?<neg>-)?+(?<hour>[0-9]{1,2})(:(?<minute>[0-5][0-9]))?+(:(?<second>[0-5][0-9]))?+").matcher("");
  95 
  96     /**
  97      * Constant for MJD 1972-01-01.
  98      */
  99     private static final long MJD_1972_01_01 = 41317L;
 100 
 101     /**
 102      * Reads a set of TZDB files and builds a single combined data file.
 103      *
 104      * @param args  the arguments
 105      */
 106     public static void main(String[] args) {
 107         if (args.length < 2) {
 108             outputHelp();
 109             return;
 110         }
 111 
 112         // parse args
 113         String version = null;
 114         File baseSrcDir = null;
 115         File dstDir = null;
 116         boolean verbose = false;
 117 
 118         // parse options
 119         int i;
 120         for (i = 0; i < args.length; i++) {
 121             String arg = args[i];
 122             if (arg.startsWith("-") == false) {
 123                 break;
 124             }
 125             if ("-srcdir".equals(arg)) {
 126                 if (baseSrcDir == null && ++i < args.length) {
 127                     baseSrcDir = new File(args[i]);
 128                     continue;
 129                 }
 130             } else if ("-dstdir".equals(arg)) {
 131                 if (dstDir == null && ++i < args.length) {
 132                     dstDir = new File(args[i]);
 133                     continue;
 134                 }
 135             } else if ("-version".equals(arg)) {
 136                 if (version == null && ++i < args.length) {
 137                     version = args[i];
 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<>();
 174         if (version != null) {
 175             //  if the "version" specified, as in jdk repo, the "baseSrcDir" is
 176             //  the "srcDir" that contains the tzdb data.
 177             srcDirs.add(baseSrcDir);
 178         } else {
 179             File[] dirs = baseSrcDir.listFiles();
 180             for (File dir : dirs) {
 181                 if (dir.isDirectory() && dir.getName().matches("[12][0-9]{3}[A-Za-z0-9._-]+")) {
 182                     srcDirs.add(dir);
 183                 }
 184             }
 185         }
 186         if (srcDirs.isEmpty()) {
 187             System.out.println("Source directory contains no valid source folders: " + baseSrcDir);
 188             return;
 189         }
 190         // check destination directory
 191         if (dstDir.exists() == false && dstDir.mkdirs() == false) {
 192             System.out.println("Destination directory could not be created: " + dstDir);
 193             return;
 194         }
 195         if (dstDir.isDirectory() == false) {
 196             System.out.println("Destination is not a directory: " + dstDir);
 197             return;
 198         }
 199         process(srcDirs, srcFileNames, dstDir, version, verbose);
 200         System.exit(0);
 201     }
 202 
 203     /**
 204      * Output usage text for the command line.
 205      */
 206     private static void outputHelp() {
 207         System.out.println("Usage: TzdbZoneRulesCompiler <options> <tzdb source filenames>");
 208         System.out.println("where options include:");
 209         System.out.println("   -srcdir <directory>   Where to find source directories (required)");
 210         System.out.println("   -dstdir <directory>   Where to output generated files (default srcdir)");
 211         System.out.println("   -version <version>    Specify the version, such as 2009a (optional)");
 212         System.out.println("   -help                 Print this usage message");
 213         System.out.println("   -verbose              Output verbose information during compilation");
 214         System.out.println(" There must be one directory for each version in srcdir");
 215         System.out.println(" Each directory must have the name of the version, such as 2009a");
 216         System.out.println(" Each directory must contain the unpacked tzdb files, such as asia or europe");
 217         System.out.println(" Directories must match the regex [12][0-9][0-9][0-9][A-Za-z0-9._-]+");
 218         System.out.println(" There will be one jar file for each version and one combined jar in dstdir");
 219         System.out.println(" If the version is specified, only that version is processed");
 220     }
 221 
 222     /**
 223      * Process to create the jar files.
 224      */
 225     private static void process(List<File> srcDirs, List<String> srcFileNames, File dstDir, String version, boolean verbose) {
 226         // build actual jar files
 227         Map<String, SortedMap<String, ZoneRules>> allBuiltZones = new TreeMap<>();
 228         Set<String> allRegionIds = new TreeSet<String>();
 229         Set<ZoneRules> allRules = new HashSet<ZoneRules>();
 230         Map<String, Map<String, String>> allLinks = new TreeMap<>();
 231 
 232         for (File srcDir : srcDirs) {
 233             // source files in this directory
 234             List<File> srcFiles = new ArrayList<>();
 235             for (String srcFileName : srcFileNames) {
 236                 File file = new File(srcDir, srcFileName);
 237                 if (file.exists()) {
 238                     srcFiles.add(file);
 239                 }
 240             }
 241             if (srcFiles.isEmpty()) {
 242                 continue;  // nothing to process
 243             }
 244 
 245             // compile
 246             String loopVersion = (srcDirs.size() == 1 && version != null)
 247                                  ? version : srcDir.getName();
 248             TzdbZoneRulesCompiler compiler = new TzdbZoneRulesCompiler(loopVersion, srcFiles, verbose);
 249             try {
 250                 // compile
 251                 compiler.compile();
 252                 SortedMap<String, ZoneRules> builtZones = compiler.getZones();
 253 
 254                 // output version-specific file
 255                 File dstFile = version == null ? new File(dstDir, "tzdb" + loopVersion + ".jar")
 256                                                : new File(dstDir, "tzdb.jar");
 257                 if (verbose) {
 258                     System.out.println("Outputting file: " + dstFile);
 259                 }
 260                 outputFile(dstFile, loopVersion, builtZones, compiler.links);
 261 
 262                 // create totals
 263                 allBuiltZones.put(loopVersion, builtZones);
 264                 allRegionIds.addAll(builtZones.keySet());
 265                 allRules.addAll(builtZones.values());
 266                 allLinks.put(loopVersion, compiler.links);
 267             } catch (Exception ex) {
 268                 System.out.println("Failed: " + ex.toString());
 269                 ex.printStackTrace();
 270                 System.exit(1);
 271             }
 272         }
 273 
 274         // output merged file
 275         if (version == null) {
 276             File dstFile = new File(dstDir, "tzdb-all.jar");
 277             if (verbose) {
 278                 System.out.println("Outputting combined file: " + dstFile);
 279             }
 280             outputFile(dstFile, allBuiltZones, allRegionIds, allRules, allLinks);
 281         }
 282     }
 283 
 284     /**
 285      * Outputs the file.
 286      */
 287     private static void outputFile(File dstFile,
 288                                    String version,
 289                                    SortedMap<String, ZoneRules> builtZones,
 290                                    Map<String, String> links) {
 291         Map<String, SortedMap<String, ZoneRules>> loopAllBuiltZones = new TreeMap<>();
 292         loopAllBuiltZones.put(version, builtZones);
 293         Set<String> loopAllRegionIds = new TreeSet<String>(builtZones.keySet());
 294         Set<ZoneRules> loopAllRules = new HashSet<ZoneRules>(builtZones.values());
 295         Map<String, Map<String, String>> loopAllLinks = new TreeMap<>();
 296         loopAllLinks.put(version, links);
 297         outputFile(dstFile, loopAllBuiltZones, loopAllRegionIds, loopAllRules, loopAllLinks);
 298     }
 299 
 300     /**
 301      * Outputs the file.
 302      */
 303     private static void outputFile(File dstFile,
 304                                    Map<String, SortedMap<String, ZoneRules>> allBuiltZones,
 305                                    Set<String> allRegionIds,
 306                                    Set<ZoneRules> allRules,
 307                                    Map<String, Map<String, String>> allLinks) {
 308         try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(dstFile))) {
 309             outputTZEntry(jos, allBuiltZones, allRegionIds, allRules, allLinks);
 310         } catch (Exception ex) {
 311             System.out.println("Failed: " + ex.toString());
 312             ex.printStackTrace();
 313             System.exit(1);
 314         }
 315     }
 316 
 317     /**
 318      * Outputs the timezone entry in the JAR file.
 319      */
 320     private static void outputTZEntry(JarOutputStream jos,
 321                                       Map<String, SortedMap<String, ZoneRules>> allBuiltZones,
 322                                       Set<String> allRegionIds,
 323                                       Set<ZoneRules> allRules,
 324                                       Map<String, Map<String, String>> allLinks) {
 325         // this format is not publicly specified
 326         try {
 327             jos.putNextEntry(new ZipEntry("TZDB.dat"));
 328             DataOutputStream out = new DataOutputStream(jos);
 329 
 330             // file version
 331             out.writeByte(1);
 332             // group
 333             out.writeUTF("TZDB");
 334             // versions
 335             String[] versionArray = allBuiltZones.keySet().toArray(new String[allBuiltZones.size()]);
 336             out.writeShort(versionArray.length);
 337             for (String version : versionArray) {
 338                 out.writeUTF(version);
 339             }
 340             // regions
 341             String[] regionArray = allRegionIds.toArray(new String[allRegionIds.size()]);
 342             out.writeShort(regionArray.length);
 343             for (String regionId : regionArray) {
 344                 out.writeUTF(regionId);
 345             }
 346             // rules
 347             List<ZoneRules> rulesList = new ArrayList<>(allRules);
 348             out.writeShort(rulesList.size());
 349             ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
 350             for (ZoneRules rules : rulesList) {
 351                 baos.reset();
 352                 DataOutputStream dataos = new DataOutputStream(baos);
 353                 rules.writeExternal(dataos);
 354                 dataos.close();
 355                 byte[] bytes = baos.toByteArray();
 356                 out.writeShort(bytes.length);
 357                 out.write(bytes);
 358             }
 359             // link version-region-rules
 360             for (String version : allBuiltZones.keySet()) {
 361                 out.writeShort(allBuiltZones.get(version).size());
 362                 for (Map.Entry<String, ZoneRules> entry : allBuiltZones.get(version).entrySet()) {
 363                      int regionIndex = Arrays.binarySearch(regionArray, entry.getKey());
 364                      int rulesIndex = rulesList.indexOf(entry.getValue());
 365                      out.writeShort(regionIndex);
 366                      out.writeShort(rulesIndex);
 367                 }
 368             }
 369             // alias-region
 370             for (String version : allLinks.keySet()) {
 371                 out.writeShort(allLinks.get(version).size());
 372                 for (Map.Entry<String, String> entry : allLinks.get(version).entrySet()) {
 373                      int aliasIndex = Arrays.binarySearch(regionArray, entry.getKey());
 374                      int regionIndex = Arrays.binarySearch(regionArray, entry.getValue());
 375                      out.writeShort(aliasIndex);
 376                      out.writeShort(regionIndex);
 377                 }
 378             }
 379             out.flush();
 380             jos.closeEntry();
 381         } catch (Exception ex) {
 382             System.out.println("Failed: " + ex.toString());
 383             ex.printStackTrace();
 384             System.exit(1);
 385         }
 386     }
 387 
 388     //-----------------------------------------------------------------------
 389     /** The TZDB rules. */
 390     private final Map<String, List<TZDBRule>> rules = new HashMap<>();
 391 
 392     /** The TZDB zones. */
 393     private final Map<String, List<TZDBZone>> zones = new HashMap<>();
 394     /** The TZDB links. */
 395 
 396     private final Map<String, String> links = new HashMap<>();
 397 
 398     /** The built zones. */
 399     private final SortedMap<String, ZoneRules> builtZones = new TreeMap<>();
 400 
 401 
 402     /** The version to produce. */
 403     private final String version;
 404 
 405     /** The source files. */
 406 
 407     private final List<File> sourceFiles;
 408 
 409     /** The version to produce. */
 410     private final boolean verbose;
 411 
 412     /**
 413      * Creates an instance if you want to invoke the compiler manually.
 414      *
 415      * @param version  the version, such as 2009a, not null
 416      * @param sourceFiles  the list of source files, not empty, not null
 417      * @param verbose  whether to output verbose messages
 418      */
 419     public TzdbZoneRulesCompiler(String version, List<File> sourceFiles, boolean verbose) {
 420         this.version = version;
 421         this.sourceFiles = sourceFiles;
 422         this.verbose = verbose;
 423     }
 424 
 425     /**
 426      * Compile the rules file.
 427      * <p>
 428      * Use {@link #getZones()} to retrieve the parsed data.
 429      *
 430      * @throws Exception if an error occurs
 431      */
 432     public void compile() throws Exception {
 433         printVerbose("Compiling TZDB version " + version);
 434         parseFiles();
 435         buildZoneRules();
 436         printVerbose("Compiled TZDB version " + version);
 437     }
 438 
 439     /**
 440      * Gets the parsed zone rules.
 441      *
 442      * @return the parsed zone rules, not null
 443      */
 444     public SortedMap<String, ZoneRules> getZones() {
 445         return builtZones;
 446     }
 447 
 448     /**
 449      * Parses the source files.
 450      *
 451      * @throws Exception if an error occurs
 452      */
 453     private void parseFiles() throws Exception {
 454         for (File file : sourceFiles) {
 455             printVerbose("Parsing file: " + file);
 456             parseFile(file);
 457         }
 458     }
 459 
 460     /**
 461      * Parses a source file.
 462      *
 463      * @param file  the file being read, not null
 464      * @throws Exception if an error occurs
 465      */
 466     private void parseFile(File file) throws Exception {
 467         int lineNumber = 1;
 468         String line = null;
 469         BufferedReader in = null;
 470         try {
 471             in = new BufferedReader(new FileReader(file));
 472             List<TZDBZone> openZone = null;
 473             for ( ; (line = in.readLine()) != null; lineNumber++) {
 474                 int index = line.indexOf('#');  // remove comments (doesn't handle # in quotes)
 475                 if (index >= 0) {
 476                     line = line.substring(0, index);
 477                 }
 478                 if (line.trim().length() == 0) {  // ignore blank lines
 479                     continue;
 480                 }
 481                 StringTokenizer st = new StringTokenizer(line, " \t");
 482                 if (openZone != null && Character.isWhitespace(line.charAt(0)) && st.hasMoreTokens()) {
 483                     if (parseZoneLine(st, openZone)) {
 484                         openZone = null;
 485                     }
 486                 } else {
 487                     if (st.hasMoreTokens()) {
 488                         String first = st.nextToken();
 489                         if (first.equals("Zone")) {
 490                             if (st.countTokens() < 3) {
 491                                 printVerbose("Invalid Zone line in file: " + file + ", line: " + line);
 492                                 throw new IllegalArgumentException("Invalid Zone line");
 493                             }
 494                             openZone = new ArrayList<>();
 495                             zones.put(st.nextToken(), openZone);
 496                             if (parseZoneLine(st, openZone)) {
 497                                 openZone = null;
 498                             }
 499                         } else {
 500                             openZone = null;
 501                             if (first.equals("Rule")) {
 502                                 if (st.countTokens() < 9) {
 503                                     printVerbose("Invalid Rule line in file: " + file + ", line: " + line);
 504                                     throw new IllegalArgumentException("Invalid Rule line");
 505                                 }
 506                                 parseRuleLine(st);
 507 
 508                             } else if (first.equals("Link")) {
 509                                 if (st.countTokens() < 2) {
 510                                     printVerbose("Invalid Link line in file: " + file + ", line: " + line);
 511                                     throw new IllegalArgumentException("Invalid Link line");
 512                                 }
 513                                 String realId = st.nextToken();
 514                                 String aliasId = st.nextToken();
 515                                 links.put(aliasId, realId);
 516 
 517                             } else {
 518                                 throw new IllegalArgumentException("Unknown line");
 519                             }
 520                         }
 521                     }
 522                 }
 523             }
 524         } catch (Exception ex) {
 525             throw new Exception("Failed while processing file '" + file + "' on line " + lineNumber + " '" + line + "'", ex);
 526         } finally {
 527             try {
 528                 if (in != null) {
 529                     in.close();
 530                 }
 531             } catch (Exception ex) {
 532                 // ignore NPE and IOE
 533             }
 534         }
 535     }
 536 
 537     /**
 538      * Parses a Rule line.
 539      *
 540      * @param st  the tokenizer, not null
 541      */
 542     private void parseRuleLine(StringTokenizer st) {
 543         TZDBRule rule = new TZDBRule();
 544         String name = st.nextToken();
 545         if (rules.containsKey(name) == false) {
 546             rules.put(name, new ArrayList<TZDBRule>());
 547         }
 548         rules.get(name).add(rule);
 549         rule.startYear = parseYear(st.nextToken(), 0);
 550         rule.endYear = parseYear(st.nextToken(), rule.startYear);
 551         if (rule.startYear > rule.endYear) {
 552             throw new IllegalArgumentException("Year order invalid: " + rule.startYear + " > " + rule.endYear);
 553         }
 554         parseOptional(st.nextToken());  // type is unused
 555         parseMonthDayTime(st, rule);
 556         rule.savingsAmount = parsePeriod(st.nextToken());
 557         rule.text = parseOptional(st.nextToken());
 558     }
 559 
 560     /**
 561      * Parses a Zone line.
 562      *
 563      * @param st  the tokenizer, not null
 564      * @return true if the zone is complete
 565      */
 566     private boolean parseZoneLine(StringTokenizer st, List<TZDBZone> zoneList) {
 567         TZDBZone zone = new TZDBZone();
 568         zoneList.add(zone);
 569         zone.standardOffset = parseOffset(st.nextToken());
 570         String savingsRule = parseOptional(st.nextToken());
 571         if (savingsRule == null) {
 572             zone.fixedSavingsSecs = 0;
 573             zone.savingsRule = null;
 574         } else {
 575             try {
 576                 zone.fixedSavingsSecs = parsePeriod(savingsRule);
 577                 zone.savingsRule = null;
 578             } catch (Exception ex) {
 579                 zone.fixedSavingsSecs = null;
 580                 zone.savingsRule = savingsRule;
 581             }
 582         }
 583         zone.text = st.nextToken();
 584         if (st.hasMoreTokens()) {
 585             zone.year = Integer.parseInt(st.nextToken());
 586             if (st.hasMoreTokens()) {
 587                 parseMonthDayTime(st, zone);
 588             }
 589             return false;
 590         } else {
 591             return true;
 592         }
 593     }
 594 
 595     /**
 596      * Parses a Rule line.
 597      *
 598      * @param st  the tokenizer, not null
 599      * @param mdt  the object to parse into, not null
 600      */
 601     private void parseMonthDayTime(StringTokenizer st, TZDBMonthDayTime mdt) {
 602         mdt.month = parseMonth(st.nextToken());
 603         if (st.hasMoreTokens()) {
 604             String dayRule = st.nextToken();
 605             if (dayRule.startsWith("last")) {
 606                 mdt.dayOfMonth = -1;
 607                 mdt.dayOfWeek = parseDayOfWeek(dayRule.substring(4));
 608                 mdt.adjustForwards = false;
 609             } else {
 610                 int index = dayRule.indexOf(">=");
 611                 if (index > 0) {
 612                     mdt.dayOfWeek = parseDayOfWeek(dayRule.substring(0, index));
 613                     dayRule = dayRule.substring(index + 2);
 614                 } else {
 615                     index = dayRule.indexOf("<=");
 616                     if (index > 0) {
 617                         mdt.dayOfWeek = parseDayOfWeek(dayRule.substring(0, index));
 618                         mdt.adjustForwards = false;
 619                         dayRule = dayRule.substring(index + 2);
 620                     }
 621                 }
 622                 mdt.dayOfMonth = Integer.parseInt(dayRule);
 623             }
 624             if (st.hasMoreTokens()) {
 625                 String timeStr = st.nextToken();
 626                 int secsOfDay = parseSecs(timeStr);
 627                 if (secsOfDay == 86400) {
 628                     mdt.endOfDay = true;
 629                     secsOfDay = 0;
 630                 }
 631                 LocalTime time = LocalTime.ofSecondOfDay(secsOfDay);
 632                 mdt.time = time;
 633                 mdt.timeDefinition = parseTimeDefinition(timeStr.charAt(timeStr.length() - 1));
 634             }
 635         }
 636     }
 637 
 638     private int parseYear(String str, int defaultYear) {
 639         if (YEAR.reset(str).matches()) {
 640             if (YEAR.group("min") != null) {
 641                 //return YEAR_MIN_VALUE;
 642                 return 1900;  // systemv has min
 643             } else if (YEAR.group("max") != null) {
 644                 return YEAR_MAX_VALUE;
 645             } else if (YEAR.group("only") != null) {
 646                 return defaultYear;
 647             }
 648             return Integer.parseInt(YEAR.group("year"));
 649         }
 650         throw new IllegalArgumentException("Unknown year: " + str);
 651     }
 652 
 653     private int parseMonth(String str) {
 654         if (MONTH.reset(str).matches()) {
 655             for (int moy = 1; moy < 13; moy++) {
 656                 if (MONTH.group(moy) != null) {
 657                     return moy;
 658                 }
 659             }
 660         }
 661         throw new IllegalArgumentException("Unknown month: " + str);
 662     }
 663 
 664     private int parseDayOfWeek(String str) {
 665         if (DOW.reset(str).matches()) {
 666             for (int dow = 1; dow < 8; dow++) {
 667                 if (DOW.group(dow) != null) {
 668                     return dow;
 669                 }
 670             }
 671         }
 672         throw new IllegalArgumentException("Unknown day-of-week: " + str);
 673     }
 674 
 675     private String parseOptional(String str) {
 676         return str.equals("-") ? null : str;
 677     }
 678 
 679     private int parseSecs(String str) {
 680         if (str.equals("-")) {
 681             return 0;
 682         }
 683         try {
 684             if (TIME.reset(str).find()) {
 685                 int secs = Integer.parseInt(TIME.group("hour")) * 60 * 60;
 686                 if (TIME.group("minute") != null) {
 687                     secs += Integer.parseInt(TIME.group("minute")) * 60;
 688                 }
 689                 if (TIME.group("second") != null) {
 690                     secs += Integer.parseInt(TIME.group("second"));
 691                 }
 692                 if (TIME.group("neg") != null) {
 693                     secs = -secs;
 694                 }
 695                 return secs;
 696             }
 697         } catch (NumberFormatException x) {}
 698         throw new IllegalArgumentException(str);
 699     }
 700 
 701     private ZoneOffset parseOffset(String str) {
 702         int secs = parseSecs(str);
 703         return ZoneOffset.ofTotalSeconds(secs);
 704     }
 705 
 706     private int parsePeriod(String str) {
 707         return parseSecs(str);
 708     }
 709 
 710     private TimeDefinition parseTimeDefinition(char c) {
 711         switch (c) {
 712             case 's':
 713             case 'S':
 714                 // standard time
 715                 return TimeDefinition.STANDARD;
 716             case 'u':
 717             case 'U':
 718             case 'g':
 719             case 'G':
 720             case 'z':
 721             case 'Z':
 722                 // UTC
 723                 return TimeDefinition.UTC;
 724             case 'w':
 725             case 'W':
 726             default:
 727                 // wall time
 728                 return TimeDefinition.WALL;
 729         }
 730     }
 731 
 732     //-----------------------------------------------------------------------
 733     /**
 734      * Build the rules, zones and links into real zones.
 735      *
 736      * @throws Exception if an error occurs
 737      */
 738     private void buildZoneRules() throws Exception {
 739         // build zones
 740         for (String zoneId : zones.keySet()) {
 741             printVerbose("Building zone " + zoneId);
 742             List<TZDBZone> tzdbZones = zones.get(zoneId);
 743             ZoneRulesBuilder bld = new ZoneRulesBuilder();
 744             for (TZDBZone tzdbZone : tzdbZones) {
 745                 bld = tzdbZone.addToBuilder(bld, rules);
 746             }
 747             ZoneRules buildRules = bld.toRules(zoneId);
 748             builtZones.put(zoneId, buildRules);
 749         }
 750 
 751         // build aliases
 752         for (String aliasId : links.keySet()) {
 753             String realId = links.get(aliasId);
 754             printVerbose("Linking alias " + aliasId + " to " + realId);
 755             ZoneRules realRules = builtZones.get(realId);
 756             if (realRules == null) {
 757                 realId = links.get(realId);  // try again (handle alias liked to alias)
 758                 printVerbose("Relinking alias " + aliasId + " to " + realId);
 759                 realRules = builtZones.get(realId);
 760                 if (realRules == null) {
 761                     throw new IllegalArgumentException("Alias '" + aliasId + "' links to invalid zone '" + realId + "' for '" + version + "'");
 762                 }
 763                 links.put(aliasId, realId);
 764 
 765             }
 766             builtZones.put(aliasId, realRules);
 767         }
 768 
 769         // remove UTC and GMT
 770         //builtZones.remove("UTC");
 771         //builtZones.remove("GMT");
 772         //builtZones.remove("GMT0");
 773         builtZones.remove("GMT+0");
 774         builtZones.remove("GMT-0");
 775         links.remove("GMT+0");
 776         links.remove("GMT-0");
 777     }
 778 
 779     //-----------------------------------------------------------------------
 780     /**
 781      * Prints a verbose message.
 782      *
 783      * @param message  the message, not null
 784      */
 785     private void printVerbose(String message) {
 786         if (verbose) {
 787             System.out.println(message);
 788         }
 789     }
 790 
 791     //-----------------------------------------------------------------------
 792     /**
 793      * Class representing a month-day-time in the TZDB file.
 794      */
 795     abstract class TZDBMonthDayTime {
 796         /** The month of the cutover. */
 797         int month = 1;
 798         /** The day-of-month of the cutover. */
 799         int dayOfMonth = 1;
 800         /** Whether to adjust forwards. */
 801         boolean adjustForwards = true;
 802         /** The day-of-week of the cutover. */
 803         int dayOfWeek = -1;
 804         /** The time of the cutover. */
 805         LocalTime time = LocalTime.MIDNIGHT;
 806         /** Whether this is midnight end of day. */
 807         boolean endOfDay;
 808         /** The time of the cutover. */
 809         TimeDefinition timeDefinition = TimeDefinition.WALL;
 810         void adjustToFowards(int year) {
 811             if (adjustForwards == false && dayOfMonth > 0) {
 812                 LocalDate adjustedDate = LocalDate.of(year, month, dayOfMonth).minusDays(6);
 813                 dayOfMonth = adjustedDate.getDayOfMonth();
 814                 month = adjustedDate.getMonth();
 815                 adjustForwards = true;
 816             }
 817         }
 818     }
 819 
 820     /**
 821      * Class representing a rule line in the TZDB file.
 822      */
 823     final class TZDBRule extends TZDBMonthDayTime {
 824         /** The start year. */
 825         int startYear;
 826         /** The end year. */
 827         int endYear;
 828         /** The amount of savings. */
 829         int savingsAmount;
 830         /** The text name of the zone. */
 831         String text;
 832 
 833         void addToBuilder(ZoneRulesBuilder bld) {
 834             adjustToFowards(2004);  // irrelevant, treat as leap year
 835             bld.addRuleToWindow(startYear, endYear, month, dayOfMonth, dayOfWeek, time, endOfDay, timeDefinition, savingsAmount);
 836         }
 837     }
 838 
 839     /**
 840      * Class representing a linked set of zone lines in the TZDB file.
 841      */
 842     final class TZDBZone extends TZDBMonthDayTime {
 843         /** The standard offset. */
 844         ZoneOffset standardOffset;
 845         /** The fixed savings amount. */
 846         Integer fixedSavingsSecs;
 847         /** The savings rule. */
 848         String savingsRule;
 849         /** The text name of the zone. */
 850         String text;
 851         /** The year of the cutover. */
 852         int year = YEAR_MAX_VALUE;
 853 
 854         ZoneRulesBuilder addToBuilder(ZoneRulesBuilder bld, Map<String, List<TZDBRule>> rules) {
 855             if (year != YEAR_MAX_VALUE) {
 856                 bld.addWindow(standardOffset, toDateTime(year), timeDefinition);
 857             } else {
 858                 bld.addWindowForever(standardOffset);
 859             }
 860             if (fixedSavingsSecs != null) {
 861                 bld.setFixedSavingsToWindow(fixedSavingsSecs);
 862             } else {
 863                 List<TZDBRule> tzdbRules = rules.get(savingsRule);
 864                 if (tzdbRules == null) {
 865                     throw new IllegalArgumentException("Rule not found: " + savingsRule);
 866                 }
 867                 for (TZDBRule tzdbRule : tzdbRules) {
 868                     tzdbRule.addToBuilder(bld);
 869                 }
 870             }
 871             return bld;
 872         }
 873 
 874         private LocalDateTime toDateTime(int year) {
 875             adjustToFowards(year);
 876             LocalDate date;
 877             if (dayOfMonth == -1) {
 878                 dayOfMonth = lengthOfMonth(month, isLeapYear(year));
 879                 date = LocalDate.of(year, month, dayOfMonth);
 880                 if (dayOfWeek != -1) {
 881                     date = previousOrSame(date, dayOfWeek);
 882                 }
 883             } else {
 884                 date = LocalDate.of(year, month, dayOfMonth);
 885                 if (dayOfWeek != -1) {
 886                     date = nextOrSame(date, dayOfWeek);
 887                 }
 888             }
 889             LocalDateTime ldt = LocalDateTime.of(date, time);
 890             if (endOfDay) {
 891                 ldt = ldt.plusDays(1);
 892             }
 893             return ldt;
 894         }
 895     }
 896 
 897 }