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 }