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.ByteArrayOutputStream;
62 import java.io.DataOutputStream;
63 import java.nio.charset.StandardCharsets;
64 import java.nio.file.Files;
65 import java.nio.file.Path;
66 import java.nio.file.Paths;
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.NoSuchElementException;
75 import java.util.Scanner;
76 import java.util.SortedMap;
77 import java.util.TreeMap;
78 import java.util.regex.Matcher;
79 import java.util.regex.MatchResult;
80 import java.util.regex.Pattern;
81
82 /**
83 * A compiler that reads a set of TZDB time-zone files and builds a single
84 * combined TZDB data file.
85 *
86 * @since 1.8
87 */
88 public final class TzdbZoneRulesCompiler {
89
90 public static void main(String[] args) {
91 new TzdbZoneRulesCompiler().compile(args);
92 }
93
94 private void compile(String[] args) {
95 if (args.length < 2) {
96 outputHelp();
97 return;
98 }
99 Path srcDir = null;
100 Path dstFile = null;
101 String version = null;
102 // parse args/options
103 int i;
104 for (i = 0; i < args.length; i++) {
105 String arg = args[i];
106 if (!arg.startsWith("-")) {
107 break;
108 }
109 if ("-srcdir".equals(arg)) {
110 if (srcDir == null && ++i < args.length) {
111 srcDir = Paths.get(args[i]);
112 continue;
113 }
114 } else if ("-dstfile".equals(arg)) {
115 if (dstFile == null && ++i < args.length) {
116 dstFile = Paths.get(args[i]);
117 continue;
118 }
119 } else if ("-verbose".equals(arg)) {
120 if (!verbose) {
121 verbose = true;
122 continue;
123 }
124 } else if (!"-help".equals(arg)) {
125 System.out.println("Unrecognised option: " + arg);
126 }
127 outputHelp();
128 return;
129 }
130 // check source directory
131 if (srcDir == null) {
132 System.err.println("Source directory must be specified using -srcdir");
133 System.exit(1);
134 }
135 if (!Files.isDirectory(srcDir)) {
136 System.err.println("Source does not exist or is not a directory: " + srcDir);
137 System.exit(1);
138 }
139 // parse source file names
140 if (i == args.length) {
141 i = 0;
142 args = new String[] {"africa", "antarctica", "asia", "australasia", "europe",
143 "northamerica","southamerica", "backward", "etcetera" };
144 System.out.println("Source filenames not specified, using default set ( ");
145 for (String name : args) {
146 System.out.printf(name + " ");
147 }
148 System.out.println(")");
149 }
150 // source files in this directory
151 List<Path> srcFiles = new ArrayList<>();
152 for (; i < args.length; i++) {
153 Path file = srcDir.resolve(args[i]);
154 if (Files.exists(file)) {
155 srcFiles.add(file);
156 } else {
157 System.err.println("Source directory does not contain source file: " + args[i]);
158 System.exit(1);
159 }
160 }
161 // check destination file
162 if (dstFile == null) {
163 dstFile = srcDir.resolve("tzdb.dat");
164 } else {
165 Path parent = dstFile.getParent();
166 if (parent != null && !Files.exists(parent)) {
167 System.err.println("Destination directory does not exist: " + parent);
168 System.exit(1);
169 }
170 }
171 try {
172 // get tzdb source version
173 Matcher m = Pattern.compile("tzdata(?<ver>[0-9]{4}[A-z])")
174 .matcher(new String(Files.readAllBytes(srcDir.resolve("VERSION")),
175 "ISO-8859-1"));
176 if (m.find()) {
177 version = m.group("ver");
178 } else {
179 System.exit(1);
180 System.err.println("Source directory does not contain file: VERSION");
181 }
182 printVerbose("Compiling TZDB version " + version);
183 // parse source files
184 for (Path file : srcFiles) {
185 printVerbose("Parsing file: " + file);
186 parseFile(file);
187 }
188 // build zone rules
189 printVerbose("Building rules");
190 buildZoneRules();
191 // output to file
192 printVerbose("Outputting tzdb file: " + dstFile);
193 outputFile(dstFile, version, builtZones, links);
194 } catch (Exception ex) {
195 System.out.println("Failed: " + ex.toString());
196 ex.printStackTrace();
197 System.exit(1);
198 }
199 System.exit(0);
200 }
201
202 /**
203 * Output usage text for the command line.
204 */
205 private static void outputHelp() {
206 System.out.println("Usage: TzdbZoneRulesCompiler <options> <tzdb source filenames>");
207 System.out.println("where options include:");
208 System.out.println(" -srcdir <directory> Where to find tzdb source directory (required)");
209 System.out.println(" -dstfile <file> Where to output generated file (default srcdir/tzdb.dat)");
210 System.out.println(" -help Print this usage message");
211 System.out.println(" -verbose Output verbose information during compilation");
212 System.out.println(" The source directory must contain the unpacked tzdb files, such as asia or europe");
213 }
214
215 /**
216 * Outputs the file.
217 */
218 private void outputFile(Path dstFile, String version,
219 SortedMap<String, ZoneRules> builtZones,
220 Map<String, String> links) {
221 try (DataOutputStream out = new DataOutputStream(Files.newOutputStream(dstFile))) {
222 // file version
223 out.writeByte(1);
224 // group
225 out.writeUTF("TZDB");
226 // versions
227 out.writeShort(1);
228 out.writeUTF(version);
229 // regions
230 String[] regionArray = builtZones.keySet().toArray(new String[builtZones.size()]);
231 out.writeShort(regionArray.length);
232 for (String regionId : regionArray) {
252 int rulesIndex = rulesList.indexOf(entry.getValue());
253 out.writeShort(regionIndex);
254 out.writeShort(rulesIndex);
255 }
256 // alias-region
257 out.writeShort(links.size());
258 for (Map.Entry<String, String> entry : links.entrySet()) {
259 int aliasIndex = Arrays.binarySearch(regionArray, entry.getKey());
260 int regionIndex = Arrays.binarySearch(regionArray, entry.getValue());
261 out.writeShort(aliasIndex);
262 out.writeShort(regionIndex);
263 }
264 out.flush();
265 } catch (Exception ex) {
266 System.out.println("Failed: " + ex.toString());
267 ex.printStackTrace();
268 System.exit(1);
269 }
270 }
271
272 private static final Pattern YEAR = Pattern.compile("(?i)(?<min>min)|(?<max>max)|(?<only>only)|(?<year>[0-9]+)");
273 private static final Pattern MONTH = Pattern.compile("(?i)(jan)|(feb)|(mar)|(apr)|(may)|(jun)|(jul)|(aug)|(sep)|(oct)|(nov)|(dec)");
274 private static final Matcher DOW = Pattern.compile("(?i)(mon)|(tue)|(wed)|(thu)|(fri)|(sat)|(sun)").matcher("");
275 private static final Matcher TIME = Pattern.compile("(?<neg>-)?+(?<hour>[0-9]{1,2})(:(?<minute>[0-5][0-9]))?+(:(?<second>[0-5][0-9]))?+").matcher("");
276
277 /** The TZDB rules. */
278 private final Map<String, List<TZDBRule>> rules = new HashMap<>();
279
280 /** The TZDB zones. */
281 private final Map<String, List<TZDBZone>> zones = new HashMap<>();
282
283 /** The TZDB links. */
284 private final Map<String, String> links = new HashMap<>();
285
286 /** The built zones. */
287 private final SortedMap<String, ZoneRules> builtZones = new TreeMap<>();
288
289 /** Whether to output verbose messages. */
290 private boolean verbose;
291
292 /**
293 * private contructor
294 */
295 private TzdbZoneRulesCompiler() {
296 }
297
298 /**
299 * Parses a source file.
300 *
301 * @param file the file being read, not null
302 * @throws Exception if an error occurs
303 */
304 private void parseFile(Path file) throws Exception {
305 int lineNumber = 1;
306 String line = null;
307 try {
308 List<String> lines = Files.readAllLines(file, StandardCharsets.ISO_8859_1);
309 List<TZDBZone> openZone = null;
310 for (; lineNumber < lines.size(); lineNumber++) {
311 line = lines.get(lineNumber);
312 int index = line.indexOf('#'); // remove comments (doesn't handle # in quotes)
313 if (index >= 0) {
314 line = line.substring(0, index);
315 }
316 if (line.trim().length() == 0) { // ignore blank lines
317 continue;
318 }
319 Scanner s = new Scanner(line);
320 if (openZone != null && Character.isWhitespace(line.charAt(0)) && s.hasNext()) {
321 if (parseZoneLine(s, openZone)) {
322 openZone = null;
323 }
324 } else {
325 if (s.hasNext()) {
326 String first = s.next();
327 if (first.equals("Zone")) {
328 openZone = new ArrayList<>();
329 try {
330 zones.put(s.next(), openZone);
331 if (parseZoneLine(s, openZone)) {
332 openZone = null;
333 }
334 } catch (NoSuchElementException x) {
335 printVerbose("Invalid Zone line in file: " + file + ", line: " + line);
336 throw new IllegalArgumentException("Invalid Zone line");
337 }
338 } else {
339 openZone = null;
340 if (first.equals("Rule")) {
341 try {
342 parseRuleLine(s);
343 } catch (NoSuchElementException x) {
344 printVerbose("Invalid Rule line in file: " + file + ", line: " + line);
345 throw new IllegalArgumentException("Invalid Rule line");
346 }
347 } else if (first.equals("Link")) {
348 try {
349 String realId = s.next();
350 String aliasId = s.next();
351 links.put(aliasId, realId);
352 } catch (NoSuchElementException x) {
353 printVerbose("Invalid Link line in file: " + file + ", line: " + line);
354 throw new IllegalArgumentException("Invalid Link line");
355 }
356
357 } else {
358 throw new IllegalArgumentException("Unknown line");
359 }
360 }
361 }
362 }
363 }
364 } catch (Exception ex) {
365 throw new Exception("Failed while parsing file '" + file + "' on line " + lineNumber + " '" + line + "'", ex);
366 }
367 }
368
369 /**
370 * Parses a Rule line.
371 *
372 * @param s the line scanner, not null
373 */
374 private void parseRuleLine(Scanner s) {
375 TZDBRule rule = new TZDBRule();
376 String name = s.next();
377 if (rules.containsKey(name) == false) {
378 rules.put(name, new ArrayList<TZDBRule>());
379 }
380 rules.get(name).add(rule);
381 rule.startYear = parseYear(s, 0);
382 rule.endYear = parseYear(s, rule.startYear);
383 if (rule.startYear > rule.endYear) {
384 throw new IllegalArgumentException("Year order invalid: " + rule.startYear + " > " + rule.endYear);
385 }
386 parseOptional(s.next()); // type is unused
387 parseMonthDayTime(s, rule);
388 rule.savingsAmount = parsePeriod(s.next());
389 rule.text = parseOptional(s.next());
390 }
391
392 /**
393 * Parses a Zone line.
394 *
395 * @param s the line scanner, not null
396 * @return true if the zone is complete
397 */
398 private boolean parseZoneLine(Scanner s, List<TZDBZone> zoneList) {
399 TZDBZone zone = new TZDBZone();
400 zoneList.add(zone);
401 zone.standardOffset = parseOffset(s.next());
402 String savingsRule = parseOptional(s.next());
403 if (savingsRule == null) {
404 zone.fixedSavingsSecs = 0;
405 zone.savingsRule = null;
406 } else {
407 try {
408 zone.fixedSavingsSecs = parsePeriod(savingsRule);
409 zone.savingsRule = null;
410 } catch (Exception ex) {
411 zone.fixedSavingsSecs = null;
412 zone.savingsRule = savingsRule;
413 }
414 }
415 zone.text = s.next();
416 if (s.hasNext()) {
417 zone.year = Integer.parseInt(s.next());
418 if (s.hasNext()) {
419 parseMonthDayTime(s, zone);
420 }
421 return false;
422 } else {
423 return true;
424 }
425 }
426
427 /**
428 * Parses a Rule line.
429 *
430 * @param s the line scanner, not null
431 * @param mdt the object to parse into, not null
432 */
433 private void parseMonthDayTime(Scanner s, TZDBMonthDayTime mdt) {
434 mdt.month = parseMonth(s);
435 if (s.hasNext()) {
436 String dayRule = s.next();
437 if (dayRule.startsWith("last")) {
438 mdt.dayOfMonth = -1;
439 mdt.dayOfWeek = parseDayOfWeek(dayRule.substring(4));
440 mdt.adjustForwards = false;
441 } else {
442 int index = dayRule.indexOf(">=");
443 if (index > 0) {
444 mdt.dayOfWeek = parseDayOfWeek(dayRule.substring(0, index));
445 dayRule = dayRule.substring(index + 2);
446 } else {
447 index = dayRule.indexOf("<=");
448 if (index > 0) {
449 mdt.dayOfWeek = parseDayOfWeek(dayRule.substring(0, index));
450 mdt.adjustForwards = false;
451 dayRule = dayRule.substring(index + 2);
452 }
453 }
454 mdt.dayOfMonth = Integer.parseInt(dayRule);
455 }
456 if (s.hasNext()) {
457 String timeStr = s.next();
458 int secsOfDay = parseSecs(timeStr);
459 if (secsOfDay == 86400) {
460 mdt.endOfDay = true;
461 secsOfDay = 0;
462 }
463 LocalTime time = LocalTime.ofSecondOfDay(secsOfDay);
464 mdt.time = time;
465 mdt.timeDefinition = parseTimeDefinition(timeStr.charAt(timeStr.length() - 1));
466 }
467 }
468 }
469
470 private int parseYear(Scanner s, int defaultYear) {
471 if (s.hasNext(YEAR)) {
472 s.next(YEAR);
473 MatchResult mr = s.match();
474 if (mr.group(1) != null) {
475 return 1900; // systemv has min
476 } else if (mr.group(2) != null) {
477 return YEAR_MAX_VALUE;
478 } else if (mr.group(3) != null) {
479 return defaultYear;
480 }
481 return Integer.parseInt(mr.group(4));
482 /*
483 if (mr.group("min") != null) {
484 //return YEAR_MIN_VALUE;
485 return 1900; // systemv has min
486 } else if (mr.group("max") != null) {
487 return YEAR_MAX_VALUE;
488 } else if (mr.group("only") != null) {
489 return defaultYear;
490 }
491 return Integer.parseInt(mr.group("year"));
492 */
493 }
494 throw new IllegalArgumentException("Unknown year: " + s.next());
495 }
496
497 private int parseMonth(Scanner s) {
498 if (s.hasNext(MONTH)) {
499 s.next(MONTH);
500 for (int moy = 1; moy < 13; moy++) {
501 if (s.match().group(moy) != null) {
502 return moy;
503 }
504 }
505 }
506 throw new IllegalArgumentException("Unknown month: " + s.next());
507 }
508
509 private int parseDayOfWeek(String str) {
510 if (DOW.reset(str).matches()) {
511 for (int dow = 1; dow < 8; dow++) {
512 if (DOW.group(dow) != null) {
513 return dow;
514 }
515 }
516 }
517 throw new IllegalArgumentException("Unknown day-of-week: " + str);
518 }
519
520 private String parseOptional(String str) {
521 return str.equals("-") ? null : str;
522 }
523
524 private int parseSecs(String str) {
525 if (str.equals("-")) {
526 return 0;
527 }
528 try {
529 if (TIME.reset(str).find()) {
530 int secs = Integer.parseInt(TIME.group("hour")) * 60 * 60;
531 if (TIME.group("minute") != null) {
532 secs += Integer.parseInt(TIME.group("minute")) * 60;
533 }
534 if (TIME.group("second") != null) {
535 secs += Integer.parseInt(TIME.group("second"));
536 }
537 if (TIME.group("neg") != null) {
538 secs = -secs;
539 }
540 return secs;
541 }
542 } catch (NumberFormatException x) {}
543 throw new IllegalArgumentException(str);
544 }
545
546 private ZoneOffset parseOffset(String str) {
547 int secs = parseSecs(str);
548 return ZoneOffset.ofTotalSeconds(secs);
549 }
550
551 private int parsePeriod(String str) {
552 return parseSecs(str);
553 }
554
555 private TimeDefinition parseTimeDefinition(char c) {
556 switch (c) {
557 case 's':
558 case 'S':
559 // standard time
560 return TimeDefinition.STANDARD;
561 case 'u':
562 case 'U':
563 case 'g':
564 case 'G':
565 case 'z':
566 case 'Z':
567 // UTC
568 return TimeDefinition.UTC;
569 case 'w':
570 case 'W':
571 default:
572 // wall time
573 return TimeDefinition.WALL;
574 }
575 }
576
577 /**
578 * Build the rules, zones and links into real zones.
579 *
580 * @throws Exception if an error occurs
581 */
582 private void buildZoneRules() throws Exception {
583 // build zones
584 for (String zoneId : zones.keySet()) {
585 printVerbose("Building zone " + zoneId);
586 List<TZDBZone> tzdbZones = zones.get(zoneId);
587 ZoneRulesBuilder bld = new ZoneRulesBuilder();
588 for (TZDBZone tzdbZone : tzdbZones) {
589 bld = tzdbZone.addToBuilder(bld, rules);
590 }
591 builtZones.put(zoneId, bld.toRules(zoneId));
592 }
593
594 // build aliases
595 for (String aliasId : links.keySet()) {
596 String realId = links.get(aliasId);
597 printVerbose("Linking alias " + aliasId + " to " + realId);
598 ZoneRules realRules = builtZones.get(realId);
599 if (realRules == null) {
600 realId = links.get(realId); // try again (handle alias liked to alias)
601 printVerbose("Relinking alias " + aliasId + " to " + realId);
602 realRules = builtZones.get(realId);
603 if (realRules == null) {
604 throw new IllegalArgumentException("Alias '" + aliasId + "' links to invalid zone '" + realId);
605 }
606 links.put(aliasId, realId);
607 }
608 builtZones.put(aliasId, realRules);
609 }
610 // remove UTC and GMT
611 // builtZones.remove("UTC");
612 // builtZones.remove("GMT");
613 // builtZones.remove("GMT0");
614 builtZones.remove("GMT+0");
615 builtZones.remove("GMT-0");
616 links.remove("GMT+0");
617 links.remove("GMT-0");
618 // remove ROC, which is not supported in j.u.tz
619 builtZones.remove("ROC");
620 links.remove("ROC");
621 // remove EST, HST and MST. They are supported via
622 // the short-id mapping
623 builtZones.remove("EST");
624 builtZones.remove("HST");
625 builtZones.remove("MST");
626 }
627
628 /**
629 * Prints a verbose message.
630 *
631 * @param message the message, not null
632 */
633 private void printVerbose(String message) {
634 if (verbose) {
635 System.out.println(message);
636 }
637 }
638
639 /**
640 * Class representing a month-day-time in the TZDB file.
641 */
642 abstract class TZDBMonthDayTime {
643 /** The month of the cutover. */
644 int month = 1;
645 /** The day-of-month of the cutover. */
646 int dayOfMonth = 1;
647 /** Whether to adjust forwards. */
648 boolean adjustForwards = true;
649 /** The day-of-week of the cutover. */
650 int dayOfWeek = -1;
651 /** The time of the cutover. */
652 LocalTime time = LocalTime.MIDNIGHT;
653 /** Whether this is midnight end of day. */
654 boolean endOfDay;
655 /** The time of the cutover. */
656 TimeDefinition timeDefinition = TimeDefinition.WALL;
657 void adjustToFowards(int year) {
658 if (adjustForwards == false && dayOfMonth > 0) {
659 LocalDate adjustedDate = LocalDate.of(year, month, dayOfMonth).minusDays(6);
660 dayOfMonth = adjustedDate.getDayOfMonth();
661 month = adjustedDate.getMonth();
662 adjustForwards = true;
663 }
664 }
665 }
666
667 /**
668 * Class representing a rule line in the TZDB file.
669 */
670 final class TZDBRule extends TZDBMonthDayTime {
671 /** The start year. */
672 int startYear;
673 /** The end year. */
674 int endYear;
675 /** The amount of savings. */
676 int savingsAmount;
677 /** The text name of the zone. */
678 String text;
679
680 void addToBuilder(ZoneRulesBuilder bld) {
681 adjustToFowards(2004); // irrelevant, treat as leap year
682 bld.addRuleToWindow(startYear, endYear, month, dayOfMonth, dayOfWeek, time, endOfDay, timeDefinition, savingsAmount);
683 }
684 }
685
686 /**
687 * Class representing a linked set of zone lines in the TZDB file.
688 */
689 final class TZDBZone extends TZDBMonthDayTime {
690 /** The standard offset. */
691 ZoneOffset standardOffset;
692 /** The fixed savings amount. */
693 Integer fixedSavingsSecs;
694 /** The savings rule. */
695 String savingsRule;
696 /** The text name of the zone. */
697 String text;
698 /** The year of the cutover. */
699 int year = YEAR_MAX_VALUE;
700
701 ZoneRulesBuilder addToBuilder(ZoneRulesBuilder bld, Map<String, List<TZDBRule>> rules) {
702 if (year != YEAR_MAX_VALUE) {
703 bld.addWindow(standardOffset, toDateTime(year), timeDefinition);
704 } else {
705 bld.addWindowForever(standardOffset);
706 }
707 if (fixedSavingsSecs != null) {
708 bld.setFixedSavingsToWindow(fixedSavingsSecs);
709 } else {
710 List<TZDBRule> tzdbRules = rules.get(savingsRule);
711 if (tzdbRules == null) {
712 throw new IllegalArgumentException("Rule not found: " + savingsRule);
713 }
714 for (TZDBRule tzdbRule : tzdbRules) {
715 tzdbRule.addToBuilder(bld);
716 }
717 }
718 return bld;
719 }
720
721 private LocalDateTime toDateTime(int year) {
722 adjustToFowards(year);
723 LocalDate date;
724 if (dayOfMonth == -1) {
725 dayOfMonth = lengthOfMonth(month, isLeapYear(year));
726 date = LocalDate.of(year, month, dayOfMonth);
727 if (dayOfWeek != -1) {
728 date = previousOrSame(date, dayOfWeek);
729 }
730 } else {
731 date = LocalDate.of(year, month, dayOfMonth);
732 if (dayOfWeek != -1) {
733 date = nextOrSame(date, dayOfWeek);
734 }
735 }
736 LocalDateTime ldt = LocalDateTime.of(date, time);
737 if (endOfDay) {
738 ldt = ldt.plusDays(1);
739 }
740 return ldt;
741 }
742 }
743 }
|
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) {
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 }
|