Refactoring to support an upcoming change

Making various changes to support a large
upcoming commit.

Most of the changes are in TzLookupGenerator
to split up a large method that is only
going to get larger.

Bug: 72142943
Test: Ran unit tests (see tzlookup_generator/README.android)
Test: Ran update-tzdata.py, tzlookup.xml had not changed
Change-Id: Ia10992c8f83eee9d619f77e4a18ecf8612f2e384
diff --git a/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/CountryZonesFileSupport.java b/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/CountryZonesFileSupport.java
index d4b0a41..2266c92 100644
--- a/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/CountryZonesFileSupport.java
+++ b/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/CountryZonesFileSupport.java
@@ -28,11 +28,11 @@
 /**
  * A class containing utility methods for details with CountryZonesFile proto objects.
  */
-final class CountryZonesFileSupport {
+public final class CountryZonesFileSupport {
 
     private CountryZonesFileSupport() {}
 
-    static CountryZonesFile.CountryZones parseCountryZonesTextFile(String file)
+    public static CountryZonesFile.CountryZones parseCountryZonesTextFile(String file)
             throws IOException, ParseException {
         try (BufferedReader fileReader = new BufferedReader(new FileReader(file))) {
             CountryZonesFile.CountryZones.Builder builder =
diff --git a/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/Errors.java b/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/Errors.java
index caf2955..2d6ede9 100644
--- a/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/Errors.java
+++ b/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/Errors.java
@@ -25,7 +25,12 @@
  */
 final class Errors {
 
-    private boolean isFatal;
+    private final static int LEVEL_WARNING = 1;
+    private final static int LEVEL_ERROR = 2;
+    private final static int LEVEL_FATAL = 3;
+
+    private int level = 0;
+
     private final LinkedList<String> scopes = new LinkedList<>();
     private final List<String> messages = new ArrayList<>();
 
@@ -45,11 +50,23 @@
     }
 
     void addFatal(String msg) {
-        isFatal = true;
+        if (level < LEVEL_FATAL) {
+            level = LEVEL_FATAL;
+        }
+        add(msg);
+    }
+
+    void addError(String msg) {
+        if (level < LEVEL_ERROR) {
+            level = LEVEL_ERROR;
+        }
         add(msg);
     }
 
     void addWarning(String msg) {
+        if (level < LEVEL_WARNING) {
+            level = LEVEL_WARNING;
+        }
         add(msg);
     }
 
@@ -62,8 +79,12 @@
         return sb.toString();
     }
 
-    boolean isFatal() {
-        return isFatal;
+    boolean hasError() {
+        return level >= LEVEL_ERROR;
+    }
+
+    boolean hasFatal() {
+        return level >= LEVEL_FATAL;
     }
 
     private void add(String msg) {
diff --git a/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/TzLookupGenerator.java b/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/TzLookupGenerator.java
index ce604fe..bfed095 100644
--- a/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/TzLookupGenerator.java
+++ b/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/TzLookupGenerator.java
@@ -121,124 +121,11 @@
             return false;
         }
 
-        // Start constructing the output structure.
-        TzLookupFile.TimeZones timeZonesOut = new TzLookupFile.TimeZones(inputIanaVersion);
-        TzLookupFile.CountryZones countryZonesOut = new TzLookupFile.CountryZones();
-        timeZonesOut.setCountryZones(countryZonesOut);
-
-        final long offsetSampleTimeMillis = getSampleOffsetTimeMillisForData(inputIanaVersion);
-
         Errors processingErrors = new Errors();
-
-        // Process each Country.
-        for (CountryZonesFile.Country countryIn : countriesIn) {
-            String isoCode = countryIn.getIsoCode();
-            processingErrors.pushScope("country=" + isoCode);
-            try {
-                // Each Country must have >= 1 time zone.
-                List<CountryZonesFile.TimeZoneMapping> timeZonesIn =
-                        countryIn.getTimeZoneMappingsList();
-                if (timeZonesIn.isEmpty()) {
-                    processingErrors.addFatal("No time zones");
-                    continue;
-                }
-
-                // Look for duplicate time zone IDs.
-                List<String> countryTimeZoneIds = CountryZonesFileSupport.extractIds(timeZonesIn);
-                if (!Utils.allUnique(countryTimeZoneIds)) {
-                    processingErrors.addFatal("country's zones=" + countryTimeZoneIds
-                            + " contains duplicates");
-                }
-
-                // Each Country needs a default time zone ID (but we can guess in some cases).
-                String defaultTimeZoneId;
-                if (countryIn.hasDefaultTimeZoneId()) {
-                    defaultTimeZoneId = countryIn.getDefaultTimeZoneId();
-                    if (!validTimeZoneId(defaultTimeZoneId)) {
-                        processingErrors.addFatal(
-                                "Default time zone ID " + defaultTimeZoneId + " is not valid");
-                        continue;
-                    }
-                } else {
-                    if (timeZonesIn.size() > 1) {
-                        processingErrors.addFatal(
-                                "To pick a default time zone there must be a single offset group");
-                        continue;
-                    }
-                    defaultTimeZoneId = timeZonesIn.get(0).getId();
-                }
-
-                // Validate the default.
-                if (!countryTimeZoneIds.contains(defaultTimeZoneId)) {
-                    processingErrors.addFatal("defaultTimeZoneId=" + defaultTimeZoneId
-                            + " is not one of the country's zones=" + countryTimeZoneIds);
-                }
-
-                // Work out the hint for whether the country uses a zero offset from UTC.
-                // We don't care about historical use of UTC (e.g. parts of Europe like France prior
-                // to WW2) so we start looking at the beginning of "this year".
-                long startTimeMillis = getYearStartTimeMillisForData(inputIanaVersion);
-                boolean everUsesUtc = anyZonesUseUtc(countryTimeZoneIds, startTimeMillis);
-
-                // Add the country to the output structure.
-                TzLookupFile.Country countryOut =
-                        new TzLookupFile.Country(isoCode, defaultTimeZoneId, everUsesUtc);
-                countryZonesOut.addCountry(countryOut);
-
-                // Validate the country information against the equivalent information in zone.tab.
-                processingErrors.pushScope("zone.tab comparison");
-                try {
-                    List<String> zoneTabCountryTimeZoneIds =
-                            zoneTabMapping.get(isoCode.toUpperCase());
-                    if (zoneTabCountryTimeZoneIds == null) {
-                        processingErrors.addFatal("Unknown country=" + isoCode);
-                        continue;
-                    }
-
-                    // Look for unexpected duplicate time zone IDs in zone.tab
-                    if (!Utils.allUnique(zoneTabCountryTimeZoneIds)) {
-                        processingErrors.addFatal(
-                                "Duplicate time zone IDs found:" + zoneTabCountryTimeZoneIds);
-                    }
-
-                    if (!Utils.setEquals(zoneTabCountryTimeZoneIds, countryTimeZoneIds)) {
-                        processingErrors.addFatal("IANA lists " + isoCode
-                                + " as having zones: " + zoneTabCountryTimeZoneIds
-                                + ", but countryzones has " + countryTimeZoneIds);
-                        continue;
-                    }
-                } finally {
-                    processingErrors.popScope();
-                }
-
-                // Process each input time zone.
-                for (CountryZonesFile.TimeZoneMapping timeZoneIn : timeZonesIn) {
-                    processingErrors.pushScope(
-                            "id=" + timeZoneIn.getId() + ", offset=" + timeZoneIn.getUtcOffset()
-                                    + ", shownInPicker=" + timeZoneIn.getShownInPicker());
-                    try {
-                        // Validate the offset information in countryzones.
-                        validateNonDstOffset(offsetSampleTimeMillis, countryIn, timeZoneIn,
-                                processingErrors);
-
-                        String timeZoneInId = timeZoneIn.getId();
-                        boolean shownInPicker = timeZoneIn.getShownInPicker();
-                        // Add the id mapping and associated metadata.
-                        TzLookupFile.TimeZoneMapping timeZoneIdOut =
-                                new TzLookupFile.TimeZoneMapping(timeZoneInId, shownInPicker);
-                        countryOut.addTimeZoneIdentifier(timeZoneIdOut);
-                    } finally {
-                        processingErrors.popScope();
-                    }
-                }
-            } finally{
-                // End of country processing.
-                processingErrors.popScope();
-            }
-        }
-
-        if (!processingErrors.isFatal()) {
-            // Write the output structure if there wasn't a fatal error.
+        TzLookupFile.TimeZones timeZonesOut = createOutputTimeZones(
+                inputIanaVersion, zoneTabMapping, countriesIn, processingErrors);
+        if (!processingErrors.hasError()) {
+            // Write the output structure if there wasn't an error.
             logInfo("Writing " + outputFile);
             try {
                 TzLookupFile.write(timeZonesOut, outputFile);
@@ -253,12 +140,193 @@
             logInfo("Issues:\n" + processingErrors.asString());
         }
 
-        return !processingErrors.isFatal();
+        return !processingErrors.hasError();
     }
 
-    private boolean anyZonesUseUtc(List<String> countryTimeZoneIds, long startTimeMillis) {
-        for (String countryTimeZoneId : countryTimeZoneIds) {
-            BasicTimeZone timeZone = (BasicTimeZone) TimeZone.getTimeZone(countryTimeZoneId);
+    private static TzLookupFile.TimeZones createOutputTimeZones(String inputIanaVersion,
+            Map<String, List<String>> zoneTabMapping, List<CountryZonesFile.Country> countriesIn,
+            Errors processingErrors) {
+        // Start constructing the output structure.
+        TzLookupFile.TimeZones timeZonesOut = new TzLookupFile.TimeZones(inputIanaVersion);
+        TzLookupFile.CountryZones countryZonesOut = new TzLookupFile.CountryZones();
+        timeZonesOut.setCountryZones(countryZonesOut);
+
+        // The time use when sampling the offsets for a zone.
+        final long offsetSampleTimeMillis = getSampleOffsetTimeMillisForData(inputIanaVersion);
+
+        // The start time to use when working out whether a zone has used UTC.
+        // We don't care about historical use of UTC (e.g. parts of Europe like France prior
+        // to WW2) so we start looking at the beginning of "this year".
+        long everUseUtcStartTimeMillis = getYearStartTimeMillisForData(inputIanaVersion);
+
+        // Process each Country.
+        for (CountryZonesFile.Country countryIn : countriesIn) {
+            String isoCode = countryIn.getIsoCode();
+            List<String> zoneTabCountryTimeZoneIds = zoneTabMapping.get(isoCode.toUpperCase());
+            if (zoneTabCountryTimeZoneIds == null) {
+                processingErrors.addError("Country=" + isoCode + " missing from zone.tab");
+                // No point in continuing.
+                continue;
+            }
+
+            TzLookupFile.Country countryOut = processCountry(
+                    offsetSampleTimeMillis, everUseUtcStartTimeMillis, countryIn,
+                    zoneTabCountryTimeZoneIds, processingErrors);
+            if (processingErrors.hasFatal()) {
+                // Stop if there's a fatal error, continue processing countries if there are just
+                // errors.
+                break;
+            } else if (countryOut == null) {
+                continue;
+            }
+            countryZonesOut.addCountry(countryOut);
+        }
+        return timeZonesOut;
+    }
+
+    private static TzLookupFile.Country processCountry(long offsetSampleTimeMillis,
+            long everUseUtcStartTimeMillis, CountryZonesFile.Country countryIn,
+            List<String> zoneTabCountryTimeZoneIds,
+            Errors processingErrors) {
+        String isoCode = countryIn.getIsoCode();
+        processingErrors.pushScope("country=" + isoCode);
+        try {
+            // Each Country must have >= 1 time zone.
+            List<CountryZonesFile.TimeZoneMapping> timeZonesIn =
+                    countryIn.getTimeZoneMappingsList();
+            if (timeZonesIn.isEmpty()) {
+                processingErrors.addError("No time zones");
+                // No point in continuing.
+                return null;
+            }
+
+            // Look for duplicate time zone IDs.
+            List<String> countryTimeZoneIds = CountryZonesFileSupport.extractIds(timeZonesIn);
+            if (!Utils.allUnique(countryTimeZoneIds)) {
+                processingErrors.addError("country's zones=" + countryTimeZoneIds
+                        + " contains duplicates");
+                // No point in continuing.
+                return null;
+            }
+
+            // Each Country needs a default time zone ID (but we can guess in some cases).
+            String defaultTimeZoneId = determineCountryDefaultZoneId(countryIn, processingErrors);
+            if (processingErrors.hasError()) {
+                // No point in continuing.
+                return null;
+            }
+
+            // Validate the default.
+            if (!countryTimeZoneIds.contains(defaultTimeZoneId)) {
+                processingErrors.addError("defaultTimeZoneId=" + defaultTimeZoneId
+                        + " is not one of the country's zones=" + countryTimeZoneIds);
+                // No point in continuing.
+                return null;
+            }
+
+            // Validate the other zone IDs.
+            for (String countryTimeZoneId : countryTimeZoneIds) {
+                if (invalidTimeZoneId(countryTimeZoneId)) {
+                    processingErrors.addError("countryTimeZoneId=" + countryTimeZoneId
+                            + " is not a valid zone ID");
+                }
+                if (processingErrors.hasError()) {
+                    // No point in continuing.
+                    return null;
+                }
+            }
+
+            // Work out the hint for whether the country uses a zero offset from UTC.
+            boolean everUsesUtc = anyZonesUseUtc(countryTimeZoneIds, everUseUtcStartTimeMillis);
+
+            // Validate the country information against the equivalent information in zone.tab.
+            processingErrors.pushScope("zone.tab comparison");
+            try {
+                // Look for unexpected duplicate time zone IDs in zone.tab
+                if (!Utils.allUnique(zoneTabCountryTimeZoneIds)) {
+                    processingErrors.addError(
+                            "Duplicate time zone IDs found:" + zoneTabCountryTimeZoneIds);
+                    // No point in continuing.
+                    return null;
+
+                }
+
+                if (!Utils.setEquals(zoneTabCountryTimeZoneIds, countryTimeZoneIds)) {
+                    processingErrors.addError("IANA lists " + isoCode
+                            + " as having zones: " + zoneTabCountryTimeZoneIds
+                            + ", but countryzones has " + countryTimeZoneIds);
+                    // No point in continuing.
+                    return null;
+                }
+            } finally {
+                processingErrors.popScope();
+            }
+
+            // Add the country to the output structure.
+            TzLookupFile.Country countryOut =
+                    new TzLookupFile.Country(isoCode, defaultTimeZoneId, everUsesUtc);
+
+            // Process each input time zone.
+            for (CountryZonesFile.TimeZoneMapping timeZoneIn : timeZonesIn) {
+                processingErrors.pushScope(
+                        "id=" + timeZoneIn.getId() + ", offset=" + timeZoneIn.getUtcOffset()
+                                + ", shownInPicker=" + timeZoneIn.getShownInPicker());
+                try {
+                    // Validate the offset information in countryIn.
+                    validateNonDstOffset(offsetSampleTimeMillis, countryIn, timeZoneIn,
+                            processingErrors);
+
+                    String timeZoneInId = timeZoneIn.getId();
+                    boolean shownInPicker = timeZoneIn.getShownInPicker();
+
+                    // Add the id mapping and associated metadata.
+                    TzLookupFile.TimeZoneMapping timeZoneIdOut =
+                            new TzLookupFile.TimeZoneMapping(timeZoneInId, shownInPicker);
+                    countryOut.addTimeZoneIdentifier(timeZoneIdOut);
+                } finally {
+                    processingErrors.popScope();
+                }
+            }
+            return countryOut;
+        } finally{
+            // End of country processing.
+            processingErrors.popScope();
+        }
+    }
+
+    /**
+     * Determines the default zone ID for the country.
+     */
+    private static String determineCountryDefaultZoneId(
+            CountryZonesFile.Country countryIn, Errors processingErrorsOut) {
+        List<CountryZonesFile.TimeZoneMapping> timeZonesIn = countryIn.getTimeZoneMappingsList();
+        String defaultTimeZoneId;
+        if (countryIn.hasDefaultTimeZoneId()) {
+            defaultTimeZoneId = countryIn.getDefaultTimeZoneId();
+            if (invalidTimeZoneId(defaultTimeZoneId)) {
+                processingErrorsOut.addError(
+                        "Default time zone ID " + defaultTimeZoneId + " is not valid");
+                // No point in continuing.
+                return null;
+            }
+        } else {
+            if (timeZonesIn.size() > 1) {
+                processingErrorsOut.addError(
+                        "To pick a default time zone there must be a single offset group");
+                // No point in continuing.
+                return null;
+            }
+            defaultTimeZoneId = timeZonesIn.get(0).getId();
+        }
+        return defaultTimeZoneId;
+    }
+
+    /**
+     * Returns true if any of the zones use UTC after the time specified.
+     */
+    private static boolean anyZonesUseUtc(List<String> timeZoneIds, long startTimeMillis) {
+        for (String timeZoneId : timeZoneIds) {
+            BasicTimeZone timeZone = (BasicTimeZone) TimeZone.getTimeZone(timeZoneId);
             TimeZoneRule[] rules = timeZone.getTimeZoneRules(startTimeMillis);
             for (TimeZoneRule rule : rules) {
                 int utcOffset = rule.getRawOffset() + rule.getDSTSavings();
@@ -301,9 +369,9 @@
         return calendar;
     }
 
-    private static boolean validTimeZoneId(String timeZoneId) {
+    private static boolean invalidTimeZoneId(String timeZoneId) {
         TimeZone zone = TimeZone.getTimeZone(timeZoneId);
-        return !zone.getID().equals(TimeZone.UNKNOWN_ZONE_ID);
+        return !(zone instanceof BasicTimeZone) || zone.getID().equals(TimeZone.UNKNOWN_ZONE_ID);
     }
 
     private static void validateNonDstOffset(long offsetSampleTimeMillis,
@@ -325,7 +393,7 @@
         }
 
         String timeZoneIdIn = timeZoneIn.getId();
-        if (!validTimeZoneId(timeZoneIdIn)) {
+        if (invalidTimeZoneId(timeZoneIdIn)) {
             errors.addFatal("Time zone ID=" + timeZoneIdIn + " is not valid");
             return;
         }
diff --git a/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/ErrorsTest.java b/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/ErrorsTest.java
index 0b5240b..a7874b9 100644
--- a/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/ErrorsTest.java
+++ b/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/ErrorsTest.java
@@ -27,11 +27,28 @@
     public void warnings() {
         Errors errors = new Errors();
         assertTrue(errors.isEmpty());
-        assertFalse(errors.isFatal());
+        assertFalse(errors.hasError());
+        assertFalse(errors.hasFatal());
 
         errors.addWarning("Hello");
         assertFalse(errors.isEmpty());
-        assertFalse(errors.isFatal());
+        assertFalse(errors.hasError());
+        assertFalse(errors.hasFatal());
+
+        TestUtils.assertContains(errors.asString(), "Hello");
+    }
+
+    @Test
+    public void error() {
+        Errors errors = new Errors();
+        assertTrue(errors.isEmpty());
+        assertFalse(errors.hasError());
+        assertFalse(errors.hasFatal());
+
+        errors.addError("Hello");
+        assertFalse(errors.isEmpty());
+        assertTrue(errors.hasError());
+        assertFalse(errors.hasFatal());
 
         TestUtils.assertContains(errors.asString(), "Hello");
     }
@@ -40,11 +57,13 @@
     public void fatal() {
         Errors errors = new Errors();
         assertTrue(errors.isEmpty());
-        assertFalse(errors.isFatal());
+        assertFalse(errors.hasError());
+        assertFalse(errors.hasFatal());
 
         errors.addFatal("Hello");
         assertFalse(errors.isEmpty());
-        assertTrue(errors.isFatal());
+        assertTrue(errors.hasError());
+        assertTrue(errors.hasFatal());
 
         TestUtils.assertContains(errors.asString(), "Hello");
     }
@@ -53,16 +72,16 @@
     public void scope() {
         Errors errors = new Errors();
 
-        errors.addFatal("Hello");
+        errors.addWarning("Hello");
 
         errors.pushScope("Monty Python");
-        errors.addFatal("John Cleese");
+        errors.addError("John Cleese");
 
         errors.pushScope("Holy grail");
         errors.addFatal("Silly place");
         errors.popScope();
 
-        errors.addFatal("Michael Palin");
+        errors.addError("Michael Palin");
 
         errors.pushScope("Parrot sketch");
         errors.addFatal("Fjords");