1 /*
   2  * Copyright (c) 2012, 2018, 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 java.io.ByteArrayOutputStream;
  60 import java.io.DataOutputStream;
  61 import java.nio.charset.StandardCharsets;
  62 import java.nio.file.Files;
  63 import java.nio.file.Path;
  64 import java.nio.file.Paths;
  65 import java.text.ParsePosition;
  66 import java.util.ArrayList;
  67 import java.util.Arrays;
  68 import java.util.List;
  69 import java.util.Map;
  70 import java.util.NoSuchElementException;
  71 import java.util.Scanner;
  72 import java.util.SortedMap;
  73 import java.util.TreeMap;
  74 import java.util.regex.Matcher;
  75 import java.util.regex.MatchResult;
  76 import java.util.regex.Pattern;
  77 import java.util.stream.Collectors;
  78 
  79 /**
  80  * A compiler that reads a set of TZDB time-zone files and builds a single
  81  * combined TZDB data file.
  82  *
  83  * @since 1.8
  84  */
  85 public final class TzdbZoneRulesCompiler {
  86 
  87     public static void main(String[] args) {
  88         new TzdbZoneRulesCompiler().compile(args);
  89     }
  90 
  91     private void compile(String[] args) {
  92         if (args.length < 2) {
  93             outputHelp();
  94             return;
  95         }
  96         Path srcDir = null;
  97         Path dstFile = null;
  98         String version = null;
  99         // parse args/options
 100         int i;
 101         for (i = 0; i < args.length; i++) {
 102             String arg = args[i];
 103             if (!arg.startsWith("-")) {
 104                 break;
 105             }
 106             if ("-srcdir".equals(arg)) {
 107                 if (srcDir == null && ++i < args.length) {
 108                     srcDir = Paths.get(args[i]);
 109                     continue;
 110                 }
 111             } else if ("-dstfile".equals(arg)) {
 112                 if (dstFile == null && ++i < args.length) {
 113                     dstFile = Paths.get(args[i]);
 114                     continue;
 115                 }
 116             } else if ("-verbose".equals(arg)) {
 117                 if (!verbose) {
 118                     verbose = true;
 119                     continue;
 120                 }
 121             } else if (!"-help".equals(arg)) {
 122                 System.out.println("Unrecognised option: " + arg);
 123             }
 124             outputHelp();
 125             return;
 126         }
 127         // check source directory
 128         if (srcDir == null) {
 129             System.err.println("Source directory must be specified using -srcdir");
 130             System.exit(1);
 131         }
 132         if (!Files.isDirectory(srcDir)) {
 133             System.err.println("Source does not exist or is not a directory: " + srcDir);
 134             System.exit(1);
 135         }
 136         // parse source file names
 137         if (i == args.length) {
 138             i = 0;
 139             args = new String[] {"africa", "antarctica", "asia", "australasia", "europe",
 140                                  "northamerica","southamerica", "backward", "etcetera" };
 141             System.out.println("Source filenames not specified, using default set ( ");
 142             for (String name : args) {
 143                 System.out.printf(name + " ");
 144             }
 145             System.out.println(")");
 146         }
 147         // source files in this directory
 148         List<Path> srcFiles = new ArrayList<>();
 149         for (; i < args.length; i++) {
 150             Path file = srcDir.resolve(args[i]);
 151             if (Files.exists(file)) {
 152                 srcFiles.add(file);
 153             } else {
 154                 System.err.println("Source directory does not contain source file: " + args[i]);
 155                 System.exit(1);
 156             }
 157         }
 158         // check destination file
 159         if (dstFile == null) {
 160             dstFile = srcDir.resolve("tzdb.dat");
 161         } else {
 162             Path parent = dstFile.getParent();
 163             if (parent != null && !Files.exists(parent)) {
 164                 System.err.println("Destination directory does not exist: " + parent);
 165                 System.exit(1);
 166             }
 167         }
 168         try {
 169             // get tzdb source version
 170             Matcher m = Pattern.compile("tzdata(?<ver>[0-9]{4}[A-z])")
 171                                .matcher(new String(Files.readAllBytes(srcDir.resolve("VERSION")),
 172                                                    "ISO-8859-1"));
 173             if (m.find()) {
 174                 version = m.group("ver");
 175             } else {
 176                 System.exit(1);
 177                 System.err.println("Source directory does not contain file: VERSION");
 178             }
 179 
 180             // load source files
 181             printVerbose("Compiling TZDB version " + version);
 182             TzdbZoneRulesProvider provider = new TzdbZoneRulesProvider(srcFiles);
 183 
 184             // build zone rules
 185             printVerbose("Building rules");
 186 
 187             // Build the rules, zones and links into real zones.
 188             SortedMap<String, ZoneRules> builtZones = new TreeMap<>();
 189 
 190             // build zones
 191             for (String zoneId : provider.getZoneIds()) {
 192                 printVerbose("Building zone " + zoneId);
 193                 builtZones.put(zoneId, provider.getZoneRules(zoneId));
 194             }
 195 
 196             // build aliases
 197             Map<String, String> links = provider.getAliasMap();
 198             for (String aliasId : links.keySet()) {
 199                 String realId = links.get(aliasId);
 200                 printVerbose("Linking alias " + aliasId + " to " + realId);
 201                 ZoneRules realRules = builtZones.get(realId);
 202                 if (realRules == null) {
 203                     realId = links.get(realId);  // try again (handle alias liked to alias)
 204                     printVerbose("Relinking alias " + aliasId + " to " + realId);
 205                     realRules = builtZones.get(realId);
 206                     if (realRules == null) {
 207                         throw new IllegalArgumentException("Alias '" + aliasId + "' links to invalid zone '" + realId);
 208                     }
 209                     links.put(aliasId, realId);
 210                 }
 211                 builtZones.put(aliasId, realRules);
 212             }
 213 
 214             // output to file
 215             printVerbose("Outputting tzdb file: " + dstFile);
 216             outputFile(dstFile, version, builtZones, links);
 217         } catch (Exception ex) {
 218             System.out.println("Failed: " + ex.toString());
 219             ex.printStackTrace();
 220             System.exit(1);
 221         }
 222         System.exit(0);
 223     }
 224 
 225     /**
 226      * Output usage text for the command line.
 227      */
 228     private static void outputHelp() {
 229         System.out.println("Usage: TzdbZoneRulesCompiler <options> <tzdb source filenames>");
 230         System.out.println("where options include:");
 231         System.out.println("   -srcdir  <directory>  Where to find tzdb source directory (required)");
 232         System.out.println("   -dstfile <file>       Where to output generated file (default srcdir/tzdb.dat)");
 233         System.out.println("   -help                 Print this usage message");
 234         System.out.println("   -verbose              Output verbose information during compilation");
 235         System.out.println(" The source directory must contain the unpacked tzdb files, such as asia or europe");
 236     }
 237 
 238     /**
 239      * Outputs the file.
 240      */
 241     private void outputFile(Path dstFile, String version,
 242                             SortedMap<String, ZoneRules> builtZones,
 243                             Map<String, String> links) {
 244         try (DataOutputStream out = new DataOutputStream(Files.newOutputStream(dstFile))) {
 245             // file version
 246             out.writeByte(1);
 247             // group
 248             out.writeUTF("TZDB");
 249             // versions
 250             out.writeShort(1);
 251             out.writeUTF(version);
 252             // regions
 253             String[] regionArray = builtZones.keySet().toArray(new String[builtZones.size()]);
 254             out.writeShort(regionArray.length);
 255             for (String regionId : regionArray) {
 256                 out.writeUTF(regionId);
 257             }
 258             // rules  -- remove the dup
 259             List<ZoneRules> rulesList = builtZones.values().stream()
 260                 .distinct()
 261                 .collect(Collectors.toList());
 262             out.writeShort(rulesList.size());
 263             ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
 264             for (ZoneRules rules : rulesList) {
 265                 baos.reset();
 266                 DataOutputStream dataos = new DataOutputStream(baos);
 267                 rules.writeExternal(dataos);
 268                 dataos.close();
 269                 byte[] bytes = baos.toByteArray();
 270                 out.writeShort(bytes.length);
 271                 out.write(bytes);
 272             }
 273             // link version-region-rules
 274             out.writeShort(builtZones.size());
 275             for (Map.Entry<String, ZoneRules> entry : builtZones.entrySet()) {
 276                  int regionIndex = Arrays.binarySearch(regionArray, entry.getKey());
 277                  int rulesIndex = rulesList.indexOf(entry.getValue());
 278                  out.writeShort(regionIndex);
 279                  out.writeShort(rulesIndex);
 280             }
 281             // alias-region
 282             out.writeShort(links.size());
 283             for (Map.Entry<String, String> entry : links.entrySet()) {
 284                  int aliasIndex = Arrays.binarySearch(regionArray, entry.getKey());
 285                  int regionIndex = Arrays.binarySearch(regionArray, entry.getValue());
 286                  out.writeShort(aliasIndex);
 287                  out.writeShort(regionIndex);
 288             }
 289             out.flush();
 290         } catch (Exception ex) {
 291             System.out.println("Failed: " + ex.toString());
 292             ex.printStackTrace();
 293             System.exit(1);
 294         }
 295     }
 296 
 297     /** Whether to output verbose messages. */
 298     private boolean verbose;
 299 
 300     /**
 301      * private contructor
 302      */
 303     private TzdbZoneRulesCompiler() {}
 304 
 305     /**
 306      * Prints a verbose message.
 307      *
 308      * @param message  the message, not null
 309      */
 310     private void printVerbose(String message) {
 311         if (verbose) {
 312             System.out.println(message);
 313         }
 314     }
 315 }