1 /*
   2  * Copyright (c) 2014, 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 package build.tools.tzdb;
  28 
  29 import java.io.File;
  30 import java.io.BufferedReader;
  31 import java.io.InputStreamReader;
  32 import java.io.FilterInputStream;
  33 import java.io.IOException;
  34 
  35 import java.util.ArrayList;
  36 import java.util.Collections;
  37 import java.util.HashMap;
  38 import java.util.HashSet;
  39 import java.util.List;
  40 import java.util.Map;
  41 import java.util.Map.Entry;
  42 import java.util.NavigableMap;
  43 import java.util.Objects;
  44 import java.util.Set;
  45 import java.util.TreeMap;
  46 import java.util.TreeSet;
  47 import java.util.concurrent.ConcurrentHashMap;
  48 
  49 import java.time.*;
  50 import java.time.Year;
  51 import java.time.chrono.IsoChronology;
  52 import java.time.temporal.TemporalAdjusters;
  53 import java.time.zone.ZoneOffsetTransition;
  54 import java.time.zone.ZoneOffsetTransitionRule;
  55 import java.time.zone.ZoneOffsetTransitionRule.TimeDefinition;
  56 import java.time.zone.ZoneRulesException;
  57 
  58 /**
  59  * Compile and build time-zone rules from IANA timezone data
  60  *
  61  * @author Xueming Shen
  62  * @author Stephen Colebourne
  63  * @author Michael Nascimento Santos  
  64  *
  65  * @since   1.9
  66  */
  67 
  68 class TzdbZoneRulesProvider {
  69 
  70     /**
  71      * Creates an instance.
  72      *
  73      * @throws ZoneRulesException if unable to load
  74      */
  75     public TzdbZoneRulesProvider(File tzdata, List<String> list) {
  76         try {
  77              load(tzdata, list);
  78         } catch (Exception ex) {
  79             throw new ZoneRulesException("Unable to load TZDB time-zone rules", ex);
  80         }
  81     }
  82 
  83     public Set<String> getZoneIds() {
  84         return new TreeSet(regionIds);
  85     }
  86 
  87     public Map<String, String> getAliasMap() {
  88         return links;
  89     }
  90 
  91     public ZoneRules getZoneRules(String zoneId) {
  92         Object obj = zones.get(zoneId);
  93         if (obj == null) {
  94             String zoneId0 = zoneId;
  95             if (links.containsKey(zoneId)) {
  96                 zoneId = links.get(zoneId);
  97                 obj = zones.get(zoneId);
  98             }
  99             if (obj == null) {
 100                 throw new ZoneRulesException("Unknown time-zone ID: " + zoneId0);
 101             }
 102         }
 103         if (obj instanceof ZoneRules) {
 104             return (ZoneRules)obj;
 105         }
 106         try {
 107             ZoneRules zrules = buildRules(zoneId, (List<ZoneLine>)obj);
 108             zones.put(zoneId, zrules);
 109             return zrules;
 110         } catch (Exception ex) {
 111             throw new ZoneRulesException(
 112                 "Invalid binary time-zone data: TZDB:" + zoneId, ex);
 113         }
 114     }
 115 
 116     //////////////////////////////////////////////////////////////////////
 117 
 118     /**
 119      * All the regions that are available.
 120      */
 121     private List<String> regionIds = new ArrayList<>(600);
 122 
 123     /**
 124      * Zone region to rules mapping
 125      */
 126     private final Map<String, Object> zones = new ConcurrentHashMap<>();
 127 
 128     /**
 129      * compatibility list
 130      */
 131     private static String[] jdk11_backward;
 132     private static HashSet<String> excludedZones;
 133     static {
 134         jdk11_backward = new String[] {
 135             "Zone       SystemV/AST4ADT -4:00   SystemV         A%sT",
 136             "Zone       SystemV/EST5EDT -5:00   SystemV         E%sT",
 137             "Zone       SystemV/CST6CDT -6:00   SystemV         C%sT",
 138             "Zone       SystemV/MST7MDT -7:00   SystemV         M%sT",
 139             "Zone       SystemV/PST8PDT -8:00   SystemV         P%sT",
 140             "Zone       SystemV/YST9YDT -9:00   SystemV         Y%sT",
 141             "Zone       SystemV/AST4    -4:00   -               AST",
 142             "Zone       SystemV/EST5    -5:00   -               EST",
 143             "Zone       SystemV/CST6    -6:00   -               CST",
 144             "Zone       SystemV/MST7    -7:00   -               MST",
 145             "Zone       SystemV/PST8    -8:00   -               PST",
 146             "Zone       SystemV/YST9    -9:00   -               YST",
 147             "Zone       SystemV/HST10   -10:00  -               HST"
 148         };
 149 
 150         // (1) exclude EST, HST and MST. They are supported
 151         //     via the short-id mapping
 152         // (2) remove UTC and GMT
 153         // (3) remove ROC, which is not supported in j.u.tz
 154         excludedZones = new HashSet<>(10);
 155         excludedZones.add("EST");
 156         excludedZones.add("HST");
 157         excludedZones.add("MST");
 158         excludedZones.add("GMT+0");
 159         excludedZones.add("GMT-0");
 160         excludedZones.add("ROC");
 161     }
 162 
 163     private Map<String, String> links = new HashMap<>(150);
 164     private Map<String, List<RuleLine>> rules = new HashMap<>(500);
 165 
 166     private void load(File srcFile, List<String> list) throws IOException {
 167         HashSet<String> included = new HashSet<>(list);        
 168         included.add("systemv");  // needed for jdk11_backward
 169 
 170         TarInputStream.TarEntry entry = null;
 171         try (TarInputStream tar = new TarInputStream(
 172                  new java.util.zip.GZIPInputStream(
 173                      new java.io.FileInputStream(srcFile), 8192));
 174              BufferedReader br = new BufferedReader(
 175                  new InputStreamReader(tar, "iso-8859-1"))) {
 176 
 177             while ((entry = tar.getNextEntry()) != null) {
 178                 String ename = entry.getName();
 179                 if (!included.contains(ename)) {
 180                     continue;
 181                 }
 182                 List<ZoneLine> openZone = null;
 183                 String line = null;
 184                 while ((line = br.readLine()) != null) {
 185                     if (line.length() == 0 || line.charAt(0) == '#') {
 186                         continue;
 187                     }
 188                     //StringIterator itr = new StringIterator(line);
 189                     String[] tokens = split(line);
 190                     if (openZone != null &&               // continuing zone line
 191                         Character.isWhitespace(line.charAt(0)) &&
 192                         tokens.length > 0) {
 193                         ZoneLine zLine = new ZoneLine();
 194                         openZone.add(zLine);
 195                         if (zLine.parse(tokens, 0)) {
 196                             openZone = null;                       
 197                         }
 198                         continue;
 199                     }
 200                     if (line.startsWith("Zone")) {        // parse Zone line
 201                         String name = tokens[1];
 202                         if (excludedZones.contains(name)){
 203                             continue;
 204                         }
 205                         if (zones.containsKey(name)) {
 206                             throw new IllegalArgumentException(
 207                                 "Duplicated zone name in file: " + name +
 208                                 ", line: [" + line + "]");
 209                         }
 210                         openZone = new ArrayList<>(10);
 211                         zones.put(name, openZone);
 212                         regionIds.add(name);
 213                         ZoneLine zLine = new ZoneLine();
 214                         openZone.add(zLine);
 215                         if (zLine.parse(tokens, 2)) {
 216                             openZone = null;
 217                         }
 218                     } else if (line.startsWith("Rule")) { // parse Rule line
 219                         String name = tokens[1];
 220                         if (!rules.containsKey(name)) {
 221                             rules.put(name, new ArrayList<RuleLine>(10));
 222                         }
 223                         rules.get(name).add(new RuleLine().parse(tokens));
 224                     } else if (line.startsWith("Link")) { // parse link line
 225                         if (tokens.length >= 3) {
 226                             String realId = tokens[1];
 227                             String aliasId = tokens[2];
 228                             if (excludedZones.contains(aliasId)){
 229                                 continue;
 230                             }
 231                             links.put(aliasId, realId);
 232                             regionIds.add(aliasId);
 233                         } else {
 234                             throw new IllegalArgumentException(
 235                                 "Invalid Link line in file" +
 236                                 entry.getName() + ", line: [" + line + "]");
 237                         }
 238                     } else {
 239                         // skip unknown line
 240                     }
 241                 }
 242 
 243             }
 244         } catch (Exception ex) {
 245             throw new RuntimeException("Failed while processing file [" +
 246             entry != null ? entry.getName() : null + "]", ex);
 247         }
 248         // add JDK 1.1.x compatible time zone IDs
 249         for (String ln : jdk11_backward) {
 250             List<ZoneLine> zone = new ArrayList<>(1);
 251             String[] tokens = split(ln);
 252             zones.put(tokens[1], zone);
 253             regionIds.add(tokens[1]);
 254 
 255             ZoneLine zLine = new ZoneLine();
 256             zone.add(zLine);
 257             zLine.parse(tokens, 2);
 258         }
 259     }
 260 
 261     private String[] split(String str) {
 262         int off = 0;
 263         int end = str.length();
 264         ArrayList<String> list = new ArrayList<>(10);
 265         while (off < end) {
 266             char c = str.charAt(off);
 267             if (c == '\t' || c == ' ') {
 268                 off++;
 269                 continue;
 270             }
 271             if (c == '#') {    // comment
 272                 break;
 273             }
 274             int start = off;
 275             while (off < end) {
 276                 c = str.charAt(off);
 277                 if (c == ' ' || c == '\t') {
 278                     break;
 279                 }
 280                 off++;
 281             }
 282             if (start != off) {
 283                 list.add(str.substring(start, off));
 284             }
 285         }
 286         return list.toArray(new String[list.size()]);
 287     }
 288 
 289     /**
 290      * Class representing a month-day-time in the TZDB file.
 291      */
 292     private static abstract class MonthDayTime {
 293         /** The month of the cutover. */
 294         Month month = Month.JANUARY;
 295 
 296         /** The day-of-month of the cutover. */
 297         int dayOfMonth = 1;
 298 
 299         /** Whether to adjust forwards. */
 300         boolean adjustForwards = true;
 301 
 302         /** The day-of-week of the cutover. */
 303         DayOfWeek dayOfWeek;
 304 
 305         /** The time of the cutover, in second of day */
 306         int secsOfDay = 0;
 307 
 308         /** Whether this is midnight end of day. */
 309         boolean endOfDay;
 310         /** The time of the cutover. */
 311 
 312         TimeDefinition timeDefinition = TimeDefinition.WALL;
 313 
 314         void adjustToForwards(int year) {
 315             if (adjustForwards == false && dayOfMonth > 0) {
 316                 // weekDay<=monthDay case, don't have it in tzdb data for now
 317                 LocalDate adjustedDate = LocalDate.of(year, month, dayOfMonth).minusDays(6);
 318                 dayOfMonth = adjustedDate.getDayOfMonth();
 319                 month = adjustedDate.getMonth();
 320                 adjustForwards = true;
 321             }
 322         }
 323 
 324         LocalDateTime toDateTime(int year) {
 325             LocalDate date;
 326             if (dayOfMonth < 0) {
 327                 int monthLen = month.length(IsoChronology.INSTANCE.isLeapYear(year));
 328                 date = LocalDate.of(year, month, monthLen + 1 + dayOfMonth);
 329                 if (dayOfWeek != null) {
 330                     date = date.with(TemporalAdjusters.previousOrSame(dayOfWeek));
 331                 }
 332             } else {
 333                 date = LocalDate.of(year, month, dayOfMonth);
 334                 if (dayOfWeek != null) {
 335                     date = date.with(TemporalAdjusters.nextOrSame(dayOfWeek));
 336                 }
 337             }
 338             if (endOfDay) {
 339                 date = date.plusDays(1);
 340             }
 341             return LocalDateTime.of(date, LocalTime.ofSecondOfDay(secsOfDay));
 342         }
 343 
 344         /**
 345          * Parses the MonthDaytime segment of a tzdb line.
 346          */
 347         private void parse(String[] tokens, int off) {
 348             month = parseMonth(tokens[off++]);
 349             if (off < tokens.length) {
 350                 String dayRule = tokens[off++];
 351                 if (dayRule.startsWith("last")) {
 352                     dayOfMonth = -1;
 353                     dayOfWeek = parseDayOfWeek(dayRule.substring(4));
 354                     adjustForwards = false;
 355                 } else {
 356                     int index = dayRule.indexOf(">=");
 357                     if (index > 0) {
 358                         dayOfWeek = parseDayOfWeek(dayRule.substring(0, index));
 359                         dayRule = dayRule.substring(index + 2);
 360                     } else {
 361                         index = dayRule.indexOf("<=");
 362                         if (index > 0) {
 363                             dayOfWeek = parseDayOfWeek(dayRule.substring(0, index));
 364                             adjustForwards = false;
 365                             dayRule = dayRule.substring(index + 2);
 366                         }
 367                     }
 368                     dayOfMonth = Integer.parseInt(dayRule);
 369                     if (dayOfMonth < -28 || dayOfMonth > 31 || dayOfMonth == 0) {
 370                        throw new IllegalArgumentException(
 371                           "Day of month indicator must be between -28 and 31 inclusive excluding zero");
 372                     }
 373                 }
 374                 if (off < tokens.length) {
 375                     String timeStr = tokens[off++];
 376                     secsOfDay = parseSecs(timeStr);
 377                     if (secsOfDay == 86400) {
 378                         // time must be midnight when end of day flag is true
 379                         endOfDay = true;
 380                         secsOfDay = 0;
 381                     }
 382                     timeDefinition = parseTimeDefinition(timeStr.charAt(timeStr.length() - 1));
 383                 }
 384             }
 385         }
 386     
 387         int parseYear(String year, int defaultYear) {
 388             switch (year.toLowerCase()) {
 389             case "min":  return 1900;
 390             case "max":  return Year.MAX_VALUE;
 391             case "only": return defaultYear;
 392             }
 393             return Integer.parseInt(year);
 394         }
 395     
 396         Month parseMonth(String mon) {
 397             switch (mon) {
 398             case "Jan": return Month.JANUARY;
 399             case "Feb": return Month.FEBRUARY;
 400             case "Mar": return Month.MARCH;
 401             case "Apr": return Month.APRIL;
 402             case "May": return Month.MAY;
 403             case "Jun": return Month.JUNE;
 404             case "Jul": return Month.JULY;
 405             case "Aug": return Month.AUGUST;
 406             case "Sep": return Month.SEPTEMBER;
 407             case "Oct": return Month.OCTOBER;
 408             case "Nov": return Month.NOVEMBER;
 409             case "Dec": return Month.DECEMBER;
 410             }
 411             throw new IllegalArgumentException("Unknown month: " + mon);
 412         }
 413     
 414         DayOfWeek parseDayOfWeek(String dow) {
 415             switch (dow) {
 416             case "Mon": return DayOfWeek.MONDAY;
 417             case "Tue": return DayOfWeek.TUESDAY;
 418             case "Wed": return DayOfWeek.WEDNESDAY;
 419             case "Thu": return DayOfWeek.THURSDAY;
 420             case "Fri": return DayOfWeek.FRIDAY;
 421             case "Sat": return DayOfWeek.SATURDAY;
 422             case "Sun": return DayOfWeek.SUNDAY;
 423             }
 424             throw new IllegalArgumentException("Unknown day-of-week: " + dow);
 425         }
 426     
 427         String parseOptional(String str) {
 428             return str.equals("-") ? null : str;
 429         }
 430 
 431         static final boolean isDigit(char c) {
 432             return c >= '0' && c <= '9';
 433         }
 434 
 435         private int parseSecs(String time) {
 436             if (time.equals("-")) {
 437                 return 0;
 438             }
 439             // faster hack
 440             int secs = 0;
 441             int sign = 1;
 442             int off = 0;
 443             int len = time.length();
 444             if (off < len && time.charAt(off) == '-') {
 445                 sign = -1;
 446                 off++;
 447             }
 448             char c0, c1;
 449             if (off < len && isDigit(c0 = time.charAt(off++))) {
 450                 int hour = c0 - '0';
 451                 if (off < len && isDigit(c1 = time.charAt(off))) {
 452                     hour = hour * 10 + c1 - '0';
 453                     off++;
 454                 }
 455                 secs = hour * 60 * 60;
 456                 if (off < len && time.charAt(off++) == ':') {
 457                     if (off + 1 < len &&
 458                         isDigit(c0 = time.charAt(off++)) &&
 459                         isDigit(c1 = time.charAt(off++))) {
 460                         // minutes
 461                         secs += ((c0 - '0') * 10 + c1 - '0') * 60;
 462                         if (off < len && time.charAt(off++) == ':') {
 463                             if (off + 1 < len &&
 464                                 isDigit(c0 = time.charAt(off++)) &&
 465                                 isDigit(c1 = time.charAt(off++))) {
 466                                 // seconds
 467                                 secs += ((c0 - '0') * 10 + c1 - '0');
 468                             }
 469                         }
 470                     }
 471 
 472                 }
 473                 return secs * sign;
 474             }
 475             throw new IllegalArgumentException("[" + time + "]");
 476         }
 477 
 478         int parseOffset(String str) {
 479             int secs = parseSecs(str);
 480             if (Math.abs(secs) > 18 * 60 * 60) {
 481                 throw new IllegalArgumentException(
 482                     "Zone offset not in valid range: -18:00 to +18:00");
 483             }
 484             return secs;
 485         }
 486     
 487         int parsePeriod(String str) {
 488             return parseSecs(str);
 489         }
 490     
 491         TimeDefinition parseTimeDefinition(char c) {
 492             switch (c) {
 493             case 's':
 494             case 'S':
 495                 // standard time
 496                 return TimeDefinition.STANDARD;
 497             case 'u':
 498             case 'U':
 499             case 'g':
 500             case 'G':
 501             case 'z':
 502             case 'Z':
 503                 // UTC
 504                 return TimeDefinition.UTC;
 505             case 'w':
 506             case 'W':
 507             default:
 508                 // wall time
 509                 return TimeDefinition.WALL;
 510             }
 511         }
 512     }
 513 
 514     /**
 515      * Class representing a rule line in the TZDB file.
 516      */
 517     private static class RuleLine extends MonthDayTime {
 518         /** The start year. */
 519         int startYear;
 520 
 521         /** The end year. */
 522         int endYear;
 523 
 524         /** The amount of savings, in seconds. */
 525         int savingsAmount;
 526 
 527         /** The text name of the zone. */
 528         String text;
 529 
 530         /**
 531          * Converts this to a transition rule.
 532          *
 533          * @param standardOffset  the active standard offset, not null
 534          * @param savingsBeforeSecs  the active savings before the transition in seconds
 535          * @return the transition, not null
 536         */ 
 537         ZoneOffsetTransitionRule toTransitionRule(ZoneOffset stdOffset, int savingsBefore) {
 538             // rule shared by different zones, so don't change it
 539             Month month = this.month;
 540             int dayOfMonth = this.dayOfMonth;
 541             DayOfWeek dayOfWeek = this.dayOfWeek;
 542             boolean endOfDay = this.endOfDay;
 543 
 544             // optimize stored format
 545             if (dayOfMonth < 0) {
 546                 if (month != Month.FEBRUARY) {    // not Month.FEBRUARY
 547                     dayOfMonth = month.maxLength() - 6;
 548                 }
 549             }
 550             if (endOfDay && dayOfMonth > 0 &&
 551                 (dayOfMonth == 28 && month == Month.FEBRUARY) == false) {
 552                 LocalDate date = LocalDate.of(2004, month, dayOfMonth).plusDays(1);  // leap-year
 553                 month = date.getMonth();
 554                 dayOfMonth = date.getDayOfMonth();
 555                 if (dayOfWeek != null) {
 556                     dayOfWeek = dayOfWeek.plus(1);
 557                 }
 558                 endOfDay = false;
 559             }
 560             // build rule
 561             return ZoneOffsetTransitionRule.of(
 562                     //month, dayOfMonth, dayOfWeek, time, endOfDay, timeDefinition,
 563                     month, dayOfMonth, dayOfWeek,
 564                     LocalTime.ofSecondOfDay(secsOfDay), endOfDay, timeDefinition,
 565                     stdOffset,
 566                     ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savingsBefore),
 567                     ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savingsAmount));
 568         }
 569 
 570         RuleLine parse(String[] tokens) {
 571             startYear = parseYear(tokens[2], 0);
 572             endYear = parseYear(tokens[3], startYear);
 573             if (startYear > endYear) {
 574                 throw new IllegalArgumentException(
 575                     "Invalid <Rule> line/Year order invalid:" + startYear + " > " + endYear);
 576             }
 577             //parseOptional(s.next());  // type is unused
 578             super.parse(tokens, 5);     // monthdaytime parsing
 579             savingsAmount = parsePeriod(tokens[8]);
 580             //rule.text = parseOptional(s.next());
 581             return this;
 582         }
 583     }
 584 
 585     /**
 586      * Class representing a linked set of zone lines in the TZDB file.
 587      */
 588     private static class ZoneLine extends MonthDayTime {
 589         /** The standard offset. */
 590         int stdOffsetSecs;
 591 
 592         /** The fixed savings amount. */
 593         int fixedSavingsSecs = 0;
 594 
 595         /** The savings rule. */
 596         String savingsRule;
 597 
 598         /** The text name of the zone. */
 599         String text;
 600 
 601         /** The cutover year */
 602         int year = Year.MAX_VALUE;
 603 
 604         /** The cutover date time */
 605         LocalDateTime ldt;
 606 
 607         /** The cutover date/time in epoch seconds/UTC */
 608         long ldtSecs = Long.MIN_VALUE;
 609 
 610         LocalDateTime toDateTime() {
 611             if (ldt == null) {
 612                 ldt = toDateTime(year);
 613             }
 614             return ldt;
 615         }
 616 
 617         /**
 618          * Creates the date-time epoch second in the wall offset for the local
 619          * date-time at the end of the window.
 620          *
 621          * @param savingsSecs  the amount of savings in use in seconds
 622          * @return the created date-time epoch second in the wall offset, not null
 623          */
 624         long toDateTimeEpochSecond(int savingsSecs) {
 625             if (ldtSecs == Long.MIN_VALUE) {
 626                 ldtSecs = toDateTime().toEpochSecond(ZoneOffset.UTC);
 627             }
 628             switch(timeDefinition) {
 629             case UTC:      return ldtSecs;
 630             case STANDARD: return ldtSecs - stdOffsetSecs;
 631             default:       return ldtSecs - (stdOffsetSecs + savingsSecs); // WALL
 632             }
 633         }
 634 
 635         boolean parse(String[] tokens, int off) {
 636             stdOffsetSecs = parseOffset(tokens[off++]);
 637             savingsRule = parseOptional(tokens[off++]);
 638             if (savingsRule != null && savingsRule.length() > 0 &&
 639                 (savingsRule.charAt(0) == '-' || isDigit(savingsRule.charAt(0)))) {
 640                 try {
 641                     fixedSavingsSecs = parsePeriod(savingsRule);
 642                     savingsRule = null;
 643                 } catch (Exception ex) {
 644                     fixedSavingsSecs = 0;
 645                 }
 646             }
 647             text = tokens[off++];
 648             if (off < tokens.length) {
 649                 year = Integer.parseInt(tokens[off++]);
 650                 if (off < tokens.length) {
 651                     super.parse(tokens, off);  // MonthDayTime
 652                 }
 653                 return false;
 654             } else {
 655                 return true;
 656             }
 657         }
 658     }    
 659 
 660     /**
 661      * Class representing a rule line in the TZDB file for a particular year.
 662      */
 663     private static class TransRule implements Comparable<TransRule>
 664     {
 665         private int year;
 666         private RuleLine rule;
 667 
 668         /** The trans date/time */
 669         private LocalDateTime ldt;
 670 
 671         /** The trans date/time in epoch seconds (assume UTC) */
 672         long ldtSecs;
 673 
 674         TransRule(int year, RuleLine rule) {
 675             this.year = year;
 676             this.rule = rule;
 677             this.ldt = rule.toDateTime(year);
 678             this.ldtSecs = ldt.toEpochSecond(ZoneOffset.UTC);
 679         }
 680 
 681         ZoneOffsetTransition toTransition(ZoneOffset standardOffset, int savingsBeforeSecs) {
 682             // copy of code in ZoneOffsetTransitionRule to avoid infinite loop
 683             ZoneOffset wallOffset = ZoneOffset.ofTotalSeconds(
 684                 standardOffset.getTotalSeconds() + savingsBeforeSecs);
 685             ZoneOffset offsetAfter = ZoneOffset.ofTotalSeconds(
 686                 standardOffset.getTotalSeconds() + rule.savingsAmount);
 687             LocalDateTime dt = rule.timeDefinition
 688                                    .createDateTime(ldt, standardOffset, wallOffset);
 689             return ZoneOffsetTransition.of(dt, wallOffset, offsetAfter);
 690         }
 691 
 692         long toEpochSecond(ZoneOffset stdOffset, int savingsBeforeSecs) {
 693             switch(rule.timeDefinition) {
 694             case UTC:      return ldtSecs;
 695             case STANDARD: return ldtSecs - stdOffset.getTotalSeconds();
 696             default:       return ldtSecs - (stdOffset.getTotalSeconds() + savingsBeforeSecs); // WALL
 697             }
 698         }
 699 
 700         /**
 701          * Tests if this a real transition with the active savings in seconds
 702          *
 703          * @param savingsBefore the active savings in seconds
 704          * @return true, if savings changes
 705          */
 706         boolean isTransition(int savingsBefore) {
 707             return rule.savingsAmount != savingsBefore;
 708         }
 709 
 710         public int compareTo(TransRule other) {
 711             return (ldtSecs < other.ldtSecs)? -1 : ((ldtSecs == other.ldtSecs) ? 0 : 1);
 712         }
 713     }
 714 
 715     private ZoneRules buildRules(String zoneId, List<ZoneLine> zones) { 
 716         if (zones.isEmpty()) {
 717             throw new IllegalStateException("No available zone window");
 718         }
 719         final List<ZoneOffsetTransition> standardTransitionList = new ArrayList<>(4);
 720         final List<ZoneOffsetTransition> transitionList = new ArrayList<>(256);
 721         final List<ZoneOffsetTransitionRule> lastTransitionRuleList = new ArrayList<>(2);
 722 
 723         final ZoneLine zone0 = zones.get(0);
 724         // initialize the standard offset, wallOffset and savings for loop
 725 
 726         //ZoneOffset stdOffset = zone0.standardOffset;
 727         ZoneOffset stdOffset = ZoneOffset.ofTotalSeconds(zone0.stdOffsetSecs);
 728 
 729         int savings = zone0.fixedSavingsSecs;
 730         ZoneOffset wallOffset = ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savings);
 731 
 732         // start ldt of each zone window
 733         LocalDateTime zoneStart = LocalDateTime.MIN;
 734 
 735         // first stanard offset
 736         ZoneOffset firstStdOffset = stdOffset;
 737         // first wall offset
 738         ZoneOffset firstWallOffset = wallOffset;
 739 
 740         for (ZoneLine zone : zones) {
 741             // check if standard offset changed, update it if yes
 742             ZoneOffset stdOffsetPrev = stdOffset;  // for effectiveSavings check
 743             if (zone.stdOffsetSecs != stdOffset.getTotalSeconds()) {
 744                 ZoneOffset stdOffsetNew = ZoneOffset.ofTotalSeconds(zone.stdOffsetSecs);
 745                 standardTransitionList.add(
 746                     ZoneOffsetTransition.of(
 747                         LocalDateTime.ofEpochSecond(zoneStart.toEpochSecond(wallOffset),
 748                                                     0,
 749                                                     stdOffset),
 750                         stdOffset,
 751                         stdOffsetNew));
 752                 stdOffset = stdOffsetNew;
 753             }
 754 
 755             LocalDateTime zoneEnd;
 756             if (zone.year == Year.MAX_VALUE) {
 757                 zoneEnd = LocalDateTime.MAX;
 758             } else {
 759                 zoneEnd = zone.toDateTime();
 760             }
 761             if (zoneEnd.compareTo(zoneStart) < 0) {
 762                 throw new IllegalStateException("Windows must be in date-time order: " +
 763                         zoneEnd + " < " + zoneStart);
 764             }
 765             // calculate effective savings at the start of the window
 766             List<TransRule> trules = null;
 767             List<TransRule> lastRules = null;
 768 
 769             int effectiveSavings = zone.fixedSavingsSecs;
 770             if (zone.savingsRule != null) {
 771                 List<RuleLine> tzdbRules = rules.get(zone.savingsRule);
 772                 if (tzdbRules == null) {
 773                    throw new IllegalArgumentException("<Rule> not found: " +
 774                                                        zone.savingsRule);
 775                 }
 776                 trules = new ArrayList<>(256);
 777                 lastRules = new ArrayList<>(2);
 778                 int lastRulesStartYear = Year.MIN_VALUE;
 779 
 780                 // merge the rules to transitions
 781                 for (RuleLine rule : tzdbRules) {
 782                     if (rule.startYear > zoneEnd.getYear()) {
 783                         // rules will not be used for this zone entry
 784                         continue;
 785                     }
 786                     rule.adjustToForwards(2004);  // irrelevant, treat as leap year
 787 
 788                     int startYear = rule.startYear;
 789                     int endYear = rule.endYear;
 790                     if (zoneEnd.equals(LocalDateTime.MAX)) {
 791                         if (endYear == Year.MAX_VALUE) {
 792                             endYear = startYear;
 793                             lastRules.add(new TransRule(endYear, rule));
 794                             lastRulesStartYear = Math.max(startYear, lastRulesStartYear);
 795                         }
 796                     } else {
 797                         if (endYear == Year.MAX_VALUE) {
 798                             //endYear = zoneEnd.getYear();
 799                             endYear = zone.year;
 800                         }
 801                     }
 802                     int year = startYear;
 803                     while (year <= endYear) {
 804                         trules.add(new TransRule(year, rule));
 805                         year++;
 806                     }
 807                 }
 808 
 809                 // last rules, fill the gap years between different last rules
 810                 if (zoneEnd.equals(LocalDateTime.MAX)) {
 811                     lastRulesStartYear = Math.max(lastRulesStartYear, zoneStart.getYear()) + 1;
 812                     for (TransRule rule : lastRules) {
 813                         if (rule.year <= lastRulesStartYear) {
 814                             int year = rule.year;
 815                             while (year <= lastRulesStartYear) {
 816                                 trules.add(new TransRule(year, rule.rule));
 817                                 year++;
 818                             }
 819                             rule.year = lastRulesStartYear;
 820                             rule.ldt = rule.rule.toDateTime(year);
 821                             rule.ldtSecs = rule.ldt.toEpochSecond(ZoneOffset.UTC);
 822                         }
 823                     }
 824                     Collections.sort(lastRules);
 825                 }
 826                 // sort the merged rules
 827                 Collections.sort(trules);
 828 
 829                 effectiveSavings = 0;
 830                 for (TransRule rule : trules) {
 831                     if (rule.toEpochSecond(stdOffsetPrev, savings) >
 832                         zoneStart.toEpochSecond(wallOffset)) {
 833                         // previous savings amount found, which could be the
 834                         // savings amount at the instant that the window starts
 835                         // (hence isAfter)
 836                         break;
 837                     }
 838                     effectiveSavings = rule.rule.savingsAmount;
 839                 }
 840             }
 841             // check if the start of the window represents a transition
 842             ZoneOffset effectiveWallOffset =
 843                 ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + effectiveSavings);
 844 
 845             if (!wallOffset.equals(effectiveWallOffset)) {
 846                 transitionList.add(ZoneOffsetTransition.of(zoneStart,
 847                                                            wallOffset,
 848                                                            effectiveWallOffset));
 849             }
 850             savings = effectiveSavings;
 851             // apply rules within the window
 852             if (trules != null) {
 853                 long zoneStartEpochSecs = zoneStart.toEpochSecond(wallOffset);
 854                 for (TransRule trule : trules) {
 855                     if (trule.isTransition(savings)) {
 856                         long epochSecs = trule.toEpochSecond(stdOffset, savings);
 857                         if (epochSecs < zoneStartEpochSecs ||
 858                             epochSecs >= zone.toDateTimeEpochSecond(savings)) {
 859                             continue;
 860                         }
 861                         transitionList.add(trule.toTransition(stdOffset, savings));
 862                         savings = trule.rule.savingsAmount;
 863                     }
 864                 }
 865             }
 866             if (lastRules != null) {
 867                 for (TransRule trule : lastRules) {
 868                     lastTransitionRuleList.add(trule.rule.toTransitionRule(stdOffset, savings));
 869                     savings = trule.rule.savingsAmount;
 870                 }
 871             }
 872 
 873             // finally we can calculate the true end of the window, passing it to the next window
 874             wallOffset = ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savings);
 875             zoneStart = LocalDateTime.ofEpochSecond(zone.toDateTimeEpochSecond(savings),
 876                                                     0,
 877                                                     wallOffset);
 878         }
 879         return new ZoneRules(firstStdOffset,
 880                              firstWallOffset,
 881                              standardTransitionList,
 882                              transitionList,
 883                              lastTransitionRuleList);
 884     }
 885 
 886 }