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 
 231         for (File srcDir : srcDirs) {
 232             // source files in this directory
 233             List<File> srcFiles = new ArrayList<>();
 234             for (String srcFileName : srcFileNames) {
 235                 File file = new File(srcDir, srcFileName);
 236                 if (file.exists()) {
 237                     srcFiles.add(file);
 238                 }
 239             }
 240             if (srcFiles.isEmpty()) {
 241                 continue;  // nothing to process
 242             }
 243 
 244             // compile
 245             String loopVersion = srcDir.getName();
 246             TzdbZoneRulesCompiler compiler = new TzdbZoneRulesCompiler(loopVersion, srcFiles, verbose);
 247             try {
 248                 // compile
 249                 compiler.compile();
 250                 SortedMap<String, ZoneRules> builtZones = compiler.getZones();
 251 
 252                 // output version-specific file
 253                 File dstFile = version == null ? new File(dstDir, "tzdb" + loopVersion + ".jar")
 254                                                : new File(dstDir, "tzdb.jar");
 255                 if (verbose) {
 256                     System.out.println("Outputting file: " + dstFile);
 257                 }
 258                 outputFile(dstFile, loopVersion, builtZones);
 259 
 260                 // create totals
 261                 allBuiltZones.put(loopVersion, builtZones);
 262                 allRegionIds.addAll(builtZones.keySet());
 263                 allRules.addAll(builtZones.values());
 264             } catch (Exception ex) {
 265                 System.out.println("Failed: " + ex.toString());
 266                 ex.printStackTrace();
 267                 System.exit(1);
 268             }
 269         }
 270 
 271         // output merged file
 272         if (version == null) {
 273             File dstFile = new File(dstDir, "tzdb-all.jar");
 274             if (verbose) {
 275                 System.out.println("Outputting combined file: " + dstFile);
 276             }
 277             outputFile(dstFile, allBuiltZones, allRegionIds, allRules);
 278         }
 279     }
 280 
 281     /**
 282      * Outputs the file.
 283      */
 284     private static void outputFile(File dstFile,
 285                                    String version,
 286                                    SortedMap<String, ZoneRules> builtZones) {
 287         Map<String, SortedMap<String, ZoneRules>> loopAllBuiltZones = new TreeMap<>();
 288         loopAllBuiltZones.put(version, builtZones);
 289         Set<String> loopAllRegionIds = new TreeSet<String>(builtZones.keySet());
 290         Set<ZoneRules> loopAllRules = new HashSet<ZoneRules>(builtZones.values());
 291         outputFile(dstFile, loopAllBuiltZones, loopAllRegionIds, loopAllRules);
 292     }
 293 
 294     /**
 295      * Outputs the file.
 296      */
 297     private static void outputFile(File dstFile,
 298                                    Map<String, SortedMap<String, ZoneRules>> allBuiltZones,
 299                                    Set<String> allRegionIds,
 300                                    Set<ZoneRules> allRules)
 301     {
 302         try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(dstFile))) {
 303             outputTZEntry(jos, allBuiltZones, allRegionIds, allRules);
 304         } catch (Exception ex) {
 305             System.out.println("Failed: " + ex.toString());
 306             ex.printStackTrace();
 307             System.exit(1);
 308         }
 309     }
 310 
 311     /**
 312      * Outputs the timezone entry in the JAR file.
 313      */
 314     private static void outputTZEntry(JarOutputStream jos,
 315                                       Map<String, SortedMap<String, ZoneRules>> allBuiltZones,
 316                                       Set<String> allRegionIds,
 317                                       Set<ZoneRules> allRules) {
 318         // this format is not publicly specified
 319         try {
 320             jos.putNextEntry(new ZipEntry("TZDB.dat"));
 321             DataOutputStream out = new DataOutputStream(jos);
 322 
 323             // file version
 324             out.writeByte(1);
 325             // group
 326             out.writeUTF("TZDB");
 327             // versions
 328             String[] versionArray = allBuiltZones.keySet().toArray(new String[allBuiltZones.size()]);
 329             out.writeShort(versionArray.length);
 330             for (String version : versionArray) {
 331                 out.writeUTF(version);
 332             }
 333             // regions
 334             String[] regionArray = allRegionIds.toArray(new String[allRegionIds.size()]);
 335             out.writeShort(regionArray.length);
 336             for (String regionId : regionArray) {
 337                 out.writeUTF(regionId);
 338             }
 339             // rules
 340             List<ZoneRules> rulesList = new ArrayList<>(allRules);
 341             out.writeShort(rulesList.size());
 342             ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
 343             for (ZoneRules rules : rulesList) {
 344                 baos.reset();
 345                 DataOutputStream dataos = new DataOutputStream(baos);
 346                 rules.writeExternal(dataos);
 347                 dataos.close();
 348                 byte[] bytes = baos.toByteArray();
 349                 out.writeShort(bytes.length);
 350                 out.write(bytes);
 351             }
 352             // link version-region-rules
 353             for (String version : allBuiltZones.keySet()) {
 354                 out.writeShort(allBuiltZones.get(version).size());
 355                 for (Map.Entry<String, ZoneRules> entry : allBuiltZones.get(version).entrySet()) {
 356                      int regionIndex = Arrays.binarySearch(regionArray, entry.getKey());
 357                      int rulesIndex = rulesList.indexOf(entry.getValue());
 358                      out.writeShort(regionIndex);
 359                      out.writeShort(rulesIndex);
 360                 }
 361             }
 362             out.flush();
 363             jos.closeEntry();
 364         } catch (Exception ex) {
 365             System.out.println("Failed: " + ex.toString());
 366             ex.printStackTrace();
 367             System.exit(1);
 368         }
 369     }
 370 
 371     //-----------------------------------------------------------------------
 372     /** The TZDB rules. */
 373     private final Map<String, List<TZDBRule>> rules = new HashMap<>();
 374 
 375     /** The TZDB zones. */
 376     private final Map<String, List<TZDBZone>> zones = new HashMap<>();
 377     /** The TZDB links. */
 378 
 379     private final Map<String, String> links = new HashMap<>();
 380 
 381     /** The built zones. */
 382     private final SortedMap<String, ZoneRules> builtZones = new TreeMap<>();
 383 
 384 
 385     /** The version to produce. */
 386     private final String version;
 387 
 388     /** The source files. */
 389 
 390     private final List<File> sourceFiles;
 391 
 392     /** The version to produce. */
 393     private final boolean verbose;
 394 
 395     /**
 396      * Creates an instance if you want to invoke the compiler manually.
 397      *
 398      * @param version  the version, such as 2009a, not null
 399      * @param sourceFiles  the list of source files, not empty, not null
 400      * @param verbose  whether to output verbose messages
 401      */
 402     public TzdbZoneRulesCompiler(String version, List<File> sourceFiles, boolean verbose) {
 403         this.version = version;
 404         this.sourceFiles = sourceFiles;
 405         this.verbose = verbose;
 406     }
 407 
 408     /**
 409      * Compile the rules file.
 410      * <p>
 411      * Use {@link #getZones()} to retrieve the parsed data.
 412      *
 413      * @throws Exception if an error occurs
 414      */
 415     public void compile() throws Exception {
 416         printVerbose("Compiling TZDB version " + version);
 417         parseFiles();
 418         buildZoneRules();
 419         printVerbose("Compiled TZDB version " + version);
 420     }
 421 
 422     /**
 423      * Gets the parsed zone rules.
 424      *
 425      * @return the parsed zone rules, not null
 426      */
 427     public SortedMap<String, ZoneRules> getZones() {
 428         return builtZones;
 429     }
 430 
 431     /**
 432      * Parses the source files.
 433      *
 434      * @throws Exception if an error occurs
 435      */
 436     private void parseFiles() throws Exception {
 437         for (File file : sourceFiles) {
 438             printVerbose("Parsing file: " + file);
 439             parseFile(file);
 440         }
 441     }
 442 
 443     /**
 444      * Parses a source file.
 445      *
 446      * @param file  the file being read, not null
 447      * @throws Exception if an error occurs
 448      */
 449     private void parseFile(File file) throws Exception {
 450         int lineNumber = 1;
 451         String line = null;
 452         BufferedReader in = null;
 453         try {
 454             in = new BufferedReader(new FileReader(file));
 455             List<TZDBZone> openZone = null;
 456             for ( ; (line = in.readLine()) != null; lineNumber++) {
 457                 int index = line.indexOf('#');  // remove comments (doesn't handle # in quotes)
 458                 if (index >= 0) {
 459                     line = line.substring(0, index);
 460                 }
 461                 if (line.trim().length() == 0) {  // ignore blank lines
 462                     continue;
 463                 }
 464                 StringTokenizer st = new StringTokenizer(line, " \t");
 465                 if (openZone != null && Character.isWhitespace(line.charAt(0)) && st.hasMoreTokens()) {
 466                     if (parseZoneLine(st, openZone)) {
 467                         openZone = null;
 468                     }
 469                 } else {
 470                     if (st.hasMoreTokens()) {
 471                         String first = st.nextToken();
 472                         if (first.equals("Zone")) {
 473                             if (st.countTokens() < 3) {
 474                                 printVerbose("Invalid Zone line in file: " + file + ", line: " + line);
 475                                 throw new IllegalArgumentException("Invalid Zone line");
 476                             }
 477                             openZone = new ArrayList<>();
 478                             zones.put(st.nextToken(), openZone);
 479                             if (parseZoneLine(st, openZone)) {
 480                                 openZone = null;
 481                             }
 482                         } else {
 483                             openZone = null;
 484                             if (first.equals("Rule")) {
 485                                 if (st.countTokens() < 9) {
 486                                     printVerbose("Invalid Rule line in file: " + file + ", line: " + line);
 487                                     throw new IllegalArgumentException("Invalid Rule line");
 488                                 }
 489                                 parseRuleLine(st);
 490 
 491                             } else if (first.equals("Link")) {
 492                                 if (st.countTokens() < 2) {
 493                                     printVerbose("Invalid Link line in file: " + file + ", line: " + line);
 494                                     throw new IllegalArgumentException("Invalid Link line");
 495                                 }
 496                                 String realId = st.nextToken();
 497                                 String aliasId = st.nextToken();
 498                                 links.put(aliasId, realId);
 499 
 500                             } else {
 501                                 throw new IllegalArgumentException("Unknown line");
 502                             }
 503                         }
 504                     }
 505                 }
 506             }
 507         } catch (Exception ex) {
 508             throw new Exception("Failed while processing file '" + file + "' on line " + lineNumber + " '" + line + "'", ex);
 509         } finally {
 510             try {
 511                 if (in != null) {
 512                     in.close();
 513                 }
 514             } catch (Exception ex) {
 515                 // ignore NPE and IOE
 516             }
 517         }
 518     }
 519 
 520     /**
 521      * Parses a Rule line.
 522      *
 523      * @param st  the tokenizer, not null
 524      */
 525     private void parseRuleLine(StringTokenizer st) {
 526         TZDBRule rule = new TZDBRule();
 527         String name = st.nextToken();
 528         if (rules.containsKey(name) == false) {
 529             rules.put(name, new ArrayList<TZDBRule>());
 530         }
 531         rules.get(name).add(rule);
 532         rule.startYear = parseYear(st.nextToken(), 0);
 533         rule.endYear = parseYear(st.nextToken(), rule.startYear);
 534         if (rule.startYear > rule.endYear) {
 535             throw new IllegalArgumentException("Year order invalid: " + rule.startYear + " > " + rule.endYear);
 536         }
 537         parseOptional(st.nextToken());  // type is unused
 538         parseMonthDayTime(st, rule);
 539         rule.savingsAmount = parsePeriod(st.nextToken());
 540         rule.text = parseOptional(st.nextToken());
 541     }
 542 
 543     /**
 544      * Parses a Zone line.
 545      *
 546      * @param st  the tokenizer, not null
 547      * @return true if the zone is complete
 548      */
 549     private boolean parseZoneLine(StringTokenizer st, List<TZDBZone> zoneList) {
 550         TZDBZone zone = new TZDBZone();
 551         zoneList.add(zone);
 552         zone.standardOffset = parseOffset(st.nextToken());
 553         String savingsRule = parseOptional(st.nextToken());
 554         if (savingsRule == null) {
 555             zone.fixedSavingsSecs = 0;
 556             zone.savingsRule = null;
 557         } else {
 558             try {
 559                 zone.fixedSavingsSecs = parsePeriod(savingsRule);
 560                 zone.savingsRule = null;
 561             } catch (Exception ex) {
 562                 zone.fixedSavingsSecs = null;
 563                 zone.savingsRule = savingsRule;
 564             }
 565         }
 566         zone.text = st.nextToken();
 567         if (st.hasMoreTokens()) {
 568             zone.year = Integer.parseInt(st.nextToken());
 569             if (st.hasMoreTokens()) {
 570                 parseMonthDayTime(st, zone);
 571             }
 572             return false;
 573         } else {
 574             return true;
 575         }
 576     }
 577 
 578     /**
 579      * Parses a Rule line.
 580      *
 581      * @param st  the tokenizer, not null
 582      * @param mdt  the object to parse into, not null
 583      */
 584     private void parseMonthDayTime(StringTokenizer st, TZDBMonthDayTime mdt) {
 585         mdt.month = parseMonth(st.nextToken());
 586         if (st.hasMoreTokens()) {
 587             String dayRule = st.nextToken();
 588             if (dayRule.startsWith("last")) {
 589                 mdt.dayOfMonth = -1;
 590                 mdt.dayOfWeek = parseDayOfWeek(dayRule.substring(4));
 591                 mdt.adjustForwards = false;
 592             } else {
 593                 int index = dayRule.indexOf(">=");
 594                 if (index > 0) {
 595                     mdt.dayOfWeek = parseDayOfWeek(dayRule.substring(0, index));
 596                     dayRule = dayRule.substring(index + 2);
 597                 } else {
 598                     index = dayRule.indexOf("<=");
 599                     if (index > 0) {
 600                         mdt.dayOfWeek = parseDayOfWeek(dayRule.substring(0, index));
 601                         mdt.adjustForwards = false;
 602                         dayRule = dayRule.substring(index + 2);
 603                     }
 604                 }
 605                 mdt.dayOfMonth = Integer.parseInt(dayRule);
 606             }
 607             if (st.hasMoreTokens()) {
 608                 String timeStr = st.nextToken();
 609                 int secsOfDay = parseSecs(timeStr);
 610                 if (secsOfDay == 86400) {
 611                     mdt.endOfDay = true;
 612                     secsOfDay = 0;
 613                 }
 614                 LocalTime time = LocalTime.ofSecondOfDay(secsOfDay);
 615                 mdt.time = time;
 616                 mdt.timeDefinition = parseTimeDefinition(timeStr.charAt(timeStr.length() - 1));
 617             }
 618         }
 619     }
 620 
 621     private int parseYear(String str, int defaultYear) {
 622         if (YEAR.reset(str).matches()) {
 623             if (YEAR.group("min") != null) {
 624                 return YEAR_MIN_VALUE;
 625             } else if (YEAR.group("max") != null) {
 626                 return YEAR_MAX_VALUE;
 627             } else if (YEAR.group("only") != null) {
 628                 return defaultYear;
 629             }
 630             return Integer.parseInt(YEAR.group("year"));
 631         }
 632         throw new IllegalArgumentException("Unknown year: " + str);
 633     }
 634 
 635     private int parseMonth(String str) {
 636         if (MONTH.reset(str).matches()) {
 637             for (int moy = 1; moy < 13; moy++) {
 638                 if (MONTH.group(moy) != null) {
 639                     return moy;
 640                 }
 641             }
 642         }
 643         throw new IllegalArgumentException("Unknown month: " + str);
 644     }
 645 
 646     private int parseDayOfWeek(String str) {
 647         if (DOW.reset(str).matches()) {
 648             for (int dow = 1; dow < 8; dow++) {
 649                 if (DOW.group(dow) != null) {
 650                     return dow;
 651                 }
 652             }
 653         }
 654         throw new IllegalArgumentException("Unknown day-of-week: " + str);
 655     }
 656 
 657     private String parseOptional(String str) {
 658         return str.equals("-") ? null : str;
 659     }
 660 
 661     private int parseSecs(String str) {
 662         if (str.equals("-")) {
 663             return 0;
 664         }
 665         try {
 666             if (TIME.reset(str).find()) {
 667                 int secs = Integer.parseInt(TIME.group("hour")) * 60 * 60;
 668                 if (TIME.group("minute") != null) {
 669                     secs += Integer.parseInt(TIME.group("minute")) * 60;
 670                 }
 671                 if (TIME.group("second") != null) {
 672                     secs += Integer.parseInt(TIME.group("second"));
 673                 }
 674                 if (TIME.group("neg") != null) {
 675                     secs = -secs;
 676                 }
 677                 return secs;
 678             }
 679         } catch (NumberFormatException x) {}
 680         throw new IllegalArgumentException(str);
 681     }
 682 
 683     private ZoneOffset parseOffset(String str) {
 684         int secs = parseSecs(str);
 685         return ZoneOffset.ofTotalSeconds(secs);
 686     }
 687 
 688     private int parsePeriod(String str) {
 689         return parseSecs(str);
 690     }
 691 
 692     private TimeDefinition parseTimeDefinition(char c) {
 693         switch (c) {
 694             case 's':
 695             case 'S':
 696                 // standard time
 697                 return TimeDefinition.STANDARD;
 698             case 'u':
 699             case 'U':
 700             case 'g':
 701             case 'G':
 702             case 'z':
 703             case 'Z':
 704                 // UTC
 705                 return TimeDefinition.UTC;
 706             case 'w':
 707             case 'W':
 708             default:
 709                 // wall time
 710                 return TimeDefinition.WALL;
 711         }
 712     }
 713 
 714     //-----------------------------------------------------------------------
 715     /**
 716      * Build the rules, zones and links into real zones.
 717      *
 718      * @throws Exception if an error occurs
 719      */
 720     private void buildZoneRules() throws Exception {
 721         // build zones
 722         for (String zoneId : zones.keySet()) {
 723             printVerbose("Building zone " + zoneId);
 724             List<TZDBZone> tzdbZones = zones.get(zoneId);
 725             ZoneRulesBuilder bld = new ZoneRulesBuilder();
 726             for (TZDBZone tzdbZone : tzdbZones) {
 727                 bld = tzdbZone.addToBuilder(bld, rules);
 728             }
 729             ZoneRules buildRules = bld.toRules(zoneId);
 730             builtZones.put(zoneId, buildRules);
 731         }
 732 
 733         // build aliases
 734         for (String aliasId : links.keySet()) {
 735             String realId = links.get(aliasId);
 736             printVerbose("Linking alias " + aliasId + " to " + realId);
 737             ZoneRules realRules = builtZones.get(realId);
 738             if (realRules == null) {
 739                 realId = links.get(realId);  // try again (handle alias liked to alias)
 740                 printVerbose("Relinking alias " + aliasId + " to " + realId);
 741                 realRules = builtZones.get(realId);
 742                 if (realRules == null) {
 743                     throw new IllegalArgumentException("Alias '" + aliasId + "' links to invalid zone '" + realId + "' for '" + version + "'");
 744                 }
 745             }
 746             builtZones.put(aliasId, realRules);
 747         }
 748 
 749         // remove UTC and GMT
 750         builtZones.remove("UTC");
 751         builtZones.remove("GMT");
 752         builtZones.remove("GMT0");
 753         builtZones.remove("GMT+0");
 754         builtZones.remove("GMT-0");
 755     }
 756 
 757     //-----------------------------------------------------------------------
 758     /**
 759      * Prints a verbose message.
 760      *
 761      * @param message  the message, not null
 762      */
 763     private void printVerbose(String message) {
 764         if (verbose) {
 765             System.out.println(message);
 766         }
 767     }
 768 
 769     //-----------------------------------------------------------------------
 770     /**
 771      * Class representing a month-day-time in the TZDB file.
 772      */
 773     abstract class TZDBMonthDayTime {
 774         /** The month of the cutover. */
 775         int month = 1;
 776         /** The day-of-month of the cutover. */
 777         int dayOfMonth = 1;
 778         /** Whether to adjust forwards. */
 779         boolean adjustForwards = true;
 780         /** The day-of-week of the cutover. */
 781         int dayOfWeek = -1;
 782         /** The time of the cutover. */
 783         LocalTime time = LocalTime.MIDNIGHT;
 784         /** Whether this is midnight end of day. */
 785         boolean endOfDay;
 786         /** The time of the cutover. */
 787         TimeDefinition timeDefinition = TimeDefinition.WALL;
 788 
 789         void adjustToFowards(int year) {
 790             if (adjustForwards == false && dayOfMonth > 0) {
 791                 LocalDate adjustedDate = LocalDate.of(year, month, dayOfMonth).minusDays(6);
 792                 dayOfMonth = adjustedDate.getDayOfMonth();
 793                 month = adjustedDate.getMonth();
 794                 adjustForwards = true;
 795             }
 796         }
 797     }
 798 
 799     /**
 800      * Class representing a rule line in the TZDB file.
 801      */
 802     final class TZDBRule extends TZDBMonthDayTime {
 803         /** The start year. */
 804         int startYear;
 805         /** The end year. */
 806         int endYear;
 807         /** The amount of savings. */
 808         int savingsAmount;
 809         /** The text name of the zone. */
 810         String text;
 811 
 812         void addToBuilder(ZoneRulesBuilder bld) {
 813             adjustToFowards(2004);  // irrelevant, treat as leap year
 814             bld.addRuleToWindow(startYear, endYear, month, dayOfMonth, dayOfWeek, time, endOfDay, timeDefinition, savingsAmount);
 815         }
 816     }
 817 
 818     /**
 819      * Class representing a linked set of zone lines in the TZDB file.
 820      */
 821     final class TZDBZone extends TZDBMonthDayTime {
 822         /** The standard offset. */
 823         ZoneOffset standardOffset;
 824         /** The fixed savings amount. */
 825         Integer fixedSavingsSecs;
 826         /** The savings rule. */
 827         String savingsRule;
 828         /** The text name of the zone. */
 829         String text;
 830         /** The year of the cutover. */
 831         int year = YEAR_MAX_VALUE;
 832 
 833         ZoneRulesBuilder addToBuilder(ZoneRulesBuilder bld, Map<String, List<TZDBRule>> rules) {
 834             if (year != YEAR_MAX_VALUE) {
 835                 bld.addWindow(standardOffset, toDateTime(year), timeDefinition);
 836             } else {
 837                 bld.addWindowForever(standardOffset);
 838             }
 839             if (fixedSavingsSecs != null) {
 840                 bld.setFixedSavingsToWindow(fixedSavingsSecs);
 841             } else {
 842                 List<TZDBRule> tzdbRules = rules.get(savingsRule);
 843                 if (tzdbRules == null) {
 844                     throw new IllegalArgumentException("Rule not found: " + savingsRule);
 845                 }
 846                 for (TZDBRule tzdbRule : tzdbRules) {
 847                     tzdbRule.addToBuilder(bld);
 848                 }
 849             }
 850             return bld;
 851         }
 852 
 853         private LocalDateTime toDateTime(int year) {
 854             adjustToFowards(year);
 855             LocalDate date;
 856             if (dayOfMonth == -1) {
 857                 dayOfMonth = lengthOfMonth(month, isLeapYear(year));
 858                 date = LocalDate.of(year, month, dayOfMonth);
 859                 if (dayOfWeek != -1) {
 860                     date = previousOrSame(date, dayOfWeek);
 861                 }
 862             } else {
 863                 date = LocalDate.of(year, month, dayOfMonth);
 864                 if (dayOfWeek != -1) {
 865                     date = nextOrSame(date, dayOfWeek);
 866                 }
 867             }
 868             LocalDateTime ldt = LocalDateTime.of(date, time);
 869             if (endOfDay) {
 870                 ldt = ldt.plusDays(1);
 871             }
 872             return ldt;
 873         }
 874     }
 875 
 876 }