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