Merge changes I9fc5738d,If4eee63b am: c206e20e48 am: dbc0aa539d am: 3a703d48fc am: 9a0b49d198
Change-Id: I0062441a24b75f74817e7ea4e002e4bbdc277afe
diff --git a/input_data/android/countryzones.txt b/input_data/android/countryzones.txt
index 25a0376..499c1f6 100644
--- a/input_data/android/countryzones.txt
+++ b/input_data/android/countryzones.txt
@@ -91,7 +91,15 @@
# TimeZoneMapping:
#
# id:
-# The ID of the time zone.
+# The ID of the time zone to use on device. See also aliasId.
+#
+# aliasId:
+# (Optional) Used to identify the modern Olson ID when the id property is
+# using an obsoleted time zone ID. A legacy time zone ID may be used on device
+# to avoid problems if the zone ID is widely used. This is intentionally
+# explicit to make it clear the use of an old ID is intentional rather than an
+# accident. The id must also link to the aliasId in IANA's data (see the IANA
+# "backward" file).
#
# utcOffset:
# The expected non-DST offset for the time zone. Used as a form of
diff --git a/input_tools/android/common/src/main/java/com/android/libcore/timezone/util/Errors.java b/input_tools/android/common/src/main/java/com/android/libcore/timezone/util/Errors.java
index 24c608c..e1e48f9 100644
--- a/input_tools/android/common/src/main/java/com/android/libcore/timezone/util/Errors.java
+++ b/input_tools/android/common/src/main/java/com/android/libcore/timezone/util/Errors.java
@@ -16,12 +16,17 @@
package com.android.libcore.timezone.util;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
- * Stores context, errors and error severity for logging and flow control.
+ * Stores context, errors and error severity for logging and flow control. This class distinguishes
+ * between warnings (just info), errors (may not be immediately fatal) and fatal (immediately
+ * fatal).
*/
public final class Errors {
@@ -45,19 +50,29 @@
return scopes.removeLast();
}
- public void addFatal(String msg) {
- level = Math.max(level, LEVEL_FATAL);
- add(msg);
+ /** Adds a fatal error, and immediately throws {@link HaltExecutionException}. */
+ public HaltExecutionException addFatalAndHalt(String msg) throws HaltExecutionException {
+ addInternal(msg, null, LEVEL_FATAL);
+ throw new HaltExecutionException("Fatal error");
+ }
+
+ /** Adds a fatal error, and immediately throws {@link HaltExecutionException}. */
+ public HaltExecutionException addFatalAndHalt(String msg, Throwable t)
+ throws HaltExecutionException {
+ addInternal(msg, t, LEVEL_FATAL);
+ throw new HaltExecutionException("Fatal error");
}
public void addError(String msg) {
- level = Math.max(level, LEVEL_ERROR);
- add(msg);
+ addInternal(msg, null, LEVEL_ERROR);
+ }
+
+ public void addError(String msg, Throwable t) {
+ addInternal(msg, t, LEVEL_ERROR);
}
public void addWarning(String msg) {
- level = Math.max(level, LEVEL_WARNING);
- add(msg);
+ addInternal(msg, null, LEVEL_WARNING);
}
public String asString() {
@@ -73,15 +88,46 @@
return messages.isEmpty();
}
+ /** True if there are error or fatal messages. */
public boolean hasError() {
return level >= LEVEL_ERROR;
}
+ /** True if there are fatal messages. */
public boolean hasFatal() {
return level >= LEVEL_FATAL;
}
- private void add(String msg) {
+ private void addInternal(String msg, Throwable t, int level) {
+ this.level = Math.max(this.level, level);
+ addMessage(msg);
+ if (t != null) {
+ try (StringWriter out = new StringWriter();
+ PrintWriter printWriter = new PrintWriter(out)) {
+ t.printStackTrace(printWriter);
+ addMessage(out.toString());
+ } catch (IOException e) {
+ // Impossible - this is actually a compiler bug. Nothing throws IOException above.
+ throw new AssertionError("Impossible exception thrown", e);
+ }
+ }
+ }
+
+ private void addMessage(String msg) {
messages.add(scopes.toString() + ": " + msg);
}
+
+ /** Throws a {@link HaltExecutionException} if there are any error or fatal messages. */
+ public void throwIfError(String why) throws HaltExecutionException {
+ if (hasError()) {
+ throw new HaltExecutionException(why);
+ }
+ }
+
+ /** Thrown to halt execution. */
+ public static class HaltExecutionException extends Exception {
+ HaltExecutionException(String why) {
+ super(why);
+ }
+ }
}
diff --git a/input_tools/android/common/src/test/java/com/android/libcore/timezone/util/ErrorsTest.java b/input_tools/android/common/src/test/java/com/android/libcore/timezone/util/ErrorsTest.java
index 5a6e717..1d568e5 100644
--- a/input_tools/android/common/src/test/java/com/android/libcore/timezone/util/ErrorsTest.java
+++ b/input_tools/android/common/src/test/java/com/android/libcore/timezone/util/ErrorsTest.java
@@ -20,8 +20,10 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import com.android.libcore.timezone.testing.TestUtils;
+import com.android.libcore.timezone.util.Errors.HaltExecutionException;
public class ErrorsTest {
@@ -62,7 +64,10 @@
assertFalse(errors.hasError());
assertFalse(errors.hasFatal());
- errors.addFatal("Hello");
+ try {
+ throw errors.addFatalAndHalt("Hello");
+ } catch (HaltExecutionException expected) {}
+
assertFalse(errors.isEmpty());
assertTrue(errors.hasError());
assertTrue(errors.hasFatal());
@@ -80,13 +85,19 @@
errors.addError("John Cleese");
errors.pushScope("Holy grail");
- errors.addFatal("Silly place");
+ try {
+ errors.addFatalAndHalt("Silly place");
+ fail();
+ } catch (HaltExecutionException expected) {}
errors.popScope();
errors.addError("Michael Palin");
errors.pushScope("Parrot sketch");
- errors.addFatal("Fjords");
+ try {
+ errors.addFatalAndHalt("Fjords");
+ fail();
+ } catch (HaltExecutionException expected) {}
errors.popScope();
String[] lines = errors.asString().split("\n");
diff --git a/input_tools/android/telephonylookup_generator/src/main/java/com/android/libcore/timezone/telephonylookup/TelephonyLookupGenerator.java b/input_tools/android/telephonylookup_generator/src/main/java/com/android/libcore/timezone/telephonylookup/TelephonyLookupGenerator.java
index 978d264..4626d8d 100644
--- a/input_tools/android/telephonylookup_generator/src/main/java/com/android/libcore/timezone/telephonylookup/TelephonyLookupGenerator.java
+++ b/input_tools/android/telephonylookup_generator/src/main/java/com/android/libcore/timezone/telephonylookup/TelephonyLookupGenerator.java
@@ -19,6 +19,7 @@
import com.android.libcore.timezone.telephonylookup.proto.TelephonyLookupProtoFile;
import com.android.libcore.timezone.util.Errors;
+import com.android.libcore.timezone.util.Errors.HaltExecutionException;
import com.ibm.icu.util.ULocale;
@@ -68,78 +69,75 @@
}
boolean execute() throws IOException {
- // Parse the countryzones input file.
- TelephonyLookupProtoFile.TelephonyLookup telephonyLookupIn;
+ Errors errors = new Errors();
try {
- telephonyLookupIn = parseTelephonyLookupTextFile(telephonyLookupProtoFile);
- } catch (ParseException e) {
- logError("Unable to parse " + telephonyLookupProtoFile, e);
- return false;
- }
+ // Parse the countryzones input file.
+ TelephonyLookupProtoFile.TelephonyLookup telephonyLookupIn;
+ try {
+ telephonyLookupIn = parseTelephonyLookupTextFile(telephonyLookupProtoFile);
+ } catch (ParseException e) {
+ throw errors.addFatalAndHalt("Unable to parse " + telephonyLookupProtoFile, e);
+ }
- List<TelephonyLookupProtoFile.Network> networksIn = telephonyLookupIn.getNetworksList();
+ List<TelephonyLookupProtoFile.Network> networksIn = telephonyLookupIn.getNetworksList();
- Errors processingErrors = new Errors();
- processingErrors.pushScope("Validation");
- validateNetworks(networksIn, processingErrors);
- processingErrors.popScope();
+ validateNetworks(networksIn, errors);
+ errors.throwIfError("One or more validation errors encountered");
- // Validation failed, so stop.
- if (processingErrors.hasFatal()) {
- logInfo("Issues:\n" + processingErrors.asString());
- return false;
- }
-
- TelephonyLookupXmlFile.TelephonyLookup telephonyLookupOut =
- createOutputTelephonyLookup(networksIn);
- if (!processingErrors.hasError()) {
- // Write the output structure if there wasn't an error.
+ TelephonyLookupXmlFile.TelephonyLookup telephonyLookupOut =
+ createOutputTelephonyLookup(networksIn);
logInfo("Writing " + outputFile);
try {
TelephonyLookupXmlFile.write(telephonyLookupOut, outputFile);
} catch (XMLStreamException e) {
- e.printStackTrace(System.err);
- processingErrors.addFatal("Unable to write output file");
+ throw errors.addFatalAndHalt("Unable to write output file", e);
+ }
+ } catch (HaltExecutionException e) {
+ e.printStackTrace();
+ logError("Stopping due to fatal error: " + e.getMessage());
+ } finally {
+ // Report all warnings / errors
+ if (!errors.isEmpty()) {
+ logInfo("Issues:\n" + errors.asString());
}
}
-
- // Report all warnings / errors
- if (!processingErrors.isEmpty()) {
- logInfo("Issues:\n" + processingErrors.asString());
- }
-
- return !processingErrors.hasError();
+ return !errors.hasError();
}
- private static void validateNetworks(
- List<TelephonyLookupProtoFile.Network> networksIn, Errors processingErrors) {
- Set<String> knownIsoCountries = getLowerCaseCountryIsoCodes();
- Set<String> mccMncSet = new HashSet<>();
- for (TelephonyLookupProtoFile.Network networkIn : networksIn) {
- String mcc = networkIn.getMcc();
- if (mcc.length() != 3 || !isAsciiNumeric(mcc)) {
- processingErrors.addFatal("mcc=" + mcc + " must have 3 decimal digits");
- }
+ private static void validateNetworks(List<TelephonyLookupProtoFile.Network> networksIn,
+ Errors errors) {
+ errors.pushScope("validateNetworks");
+ try {
+ Set<String> knownIsoCountries = getLowerCaseCountryIsoCodes();
+ Set<String> mccMncSet = new HashSet<>();
+ for (TelephonyLookupProtoFile.Network networkIn : networksIn) {
+ String mcc = networkIn.getMcc();
+ if (mcc.length() != 3 || !isAsciiNumeric(mcc)) {
+ errors.addError("mcc=" + mcc + " must have 3 decimal digits");
+ }
- String mnc = networkIn.getMnc();
- if (!(mnc.length() == 2 || mnc.length() == 3) || !isAsciiNumeric(mnc)) {
- processingErrors.addFatal("mnc=" + mnc + " must have 2 or 3 decimal digits");
- }
+ String mnc = networkIn.getMnc();
+ if (!(mnc.length() == 2 || mnc.length() == 3) || !isAsciiNumeric(mnc)) {
+ errors.addError("mnc=" + mnc + " must have 2 or 3 decimal digits");
+ }
- String mccMnc = "" + mcc + mnc;
- if (!mccMncSet.add(mccMnc)) {
- processingErrors.addFatal("Duplicate entry for mcc=" + mcc + ", mnc=" + mnc);
- }
+ String mccMnc = "" + mcc + mnc;
+ if (!mccMncSet.add(mccMnc)) {
+ errors.addError("Duplicate entry for mcc=" + mcc + ", mnc=" + mnc);
+ }
- String countryIsoCode = networkIn.getCountryIsoCode();
- String countryIsoCodeLower = countryIsoCode.toLowerCase(Locale.ROOT);
- if (!countryIsoCodeLower.equals(countryIsoCode)) {
- processingErrors.addFatal("Country code not lower case: " + countryIsoCode);
- }
+ String countryIsoCode = networkIn.getCountryIsoCode();
+ String countryIsoCodeLower = countryIsoCode.toLowerCase(Locale.ROOT);
+ if (!countryIsoCodeLower.equals(countryIsoCode)) {
+ errors.addError("Country code not lower case: " + countryIsoCode);
+ }
- if (!knownIsoCountries.contains(countryIsoCodeLower)) {
- processingErrors.addFatal("Country code not known: " + countryIsoCode);
+ if (!knownIsoCountries.contains(countryIsoCodeLower)) {
+ errors.addError("Country code not known: " + countryIsoCode);
+ }
}
+ } finally {
+ errors.popScope();
}
}
diff --git a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/BackwardFile.java b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/BackwardFile.java
new file mode 100644
index 0000000..161a6b8
--- /dev/null
+++ b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/BackwardFile.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.libcore.timezone.tzlookup;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.text.ParseException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * A class that knows about the structure of the backward file.
+ */
+final class BackwardFile {
+
+ private final Map<String, String> links = new HashMap<>();
+
+ private BackwardFile() {}
+
+ static BackwardFile parse(String backwardFile) throws IOException, ParseException {
+ BackwardFile backward = new BackwardFile();
+
+ List<String> lines = Files
+ .readAllLines(Paths.get(backwardFile), StandardCharsets.US_ASCII);
+
+ // Remove comments
+ List<String> linkLines =
+ lines.stream()
+ .filter(s -> !(s.startsWith("#") || s.isEmpty()))
+ .collect(Collectors.toList());
+
+ for (String linkLine : linkLines) {
+ String[] fields = linkLine.split("\t+");
+ if (fields.length < 3 || !fields[0].equals("Link")) {
+ throw new ParseException("Line is malformed: " + linkLine, 0);
+ }
+ backward.addLink(fields[1], fields[2]);
+ }
+ return backward;
+ }
+
+ /**
+ * Add a link entry.
+ *
+ * @param target the new tz ID
+ * @param linkName the old tz ID
+ */
+ private void addLink(String target, String linkName) {
+ String oldValue = links.put(linkName, target);
+ if (oldValue != null) {
+ throw new IllegalStateException("Duplicate link from " + linkName);
+ }
+ }
+
+ /** Returns a mapping from linkName (old tz ID) to target (new tz ID). */
+ Map<String, String> getDirectLinks() {
+ // Validate links for cycles and collapse the links if there are links to links. There's a
+ // simple check to confirm that no chain is longer than a fixed length, to guard against
+ // cycles.
+ final int maxChainLength = 2;
+ Map<String, String> collapsedLinks = new HashMap<>();
+ for (String fromId : links.keySet()) {
+ int chainLength = 0;
+ String currentId = fromId;
+ String lastId = null;
+ while ((currentId = links.get(currentId)) != null) {
+ chainLength++;
+ lastId = currentId;
+ if (chainLength >= maxChainLength) {
+ throw new IllegalStateException(
+ "Chain from " + fromId + " is longer than " + maxChainLength);
+ }
+ }
+ if (chainLength == 0) {
+ throw new IllegalStateException("Null Link targetId for " + fromId);
+ }
+ collapsedLinks.put(fromId, lastId);
+ }
+ return Collections.unmodifiableMap(collapsedLinks);
+ }
+}
diff --git a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/TzLookupGenerator.java b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/TzLookupGenerator.java
index 95525bf..b6d368b 100644
--- a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/TzLookupGenerator.java
+++ b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/TzLookupGenerator.java
@@ -19,6 +19,7 @@
import com.android.libcore.timezone.tzlookup.zonetree.CountryZoneTree;
import com.android.libcore.timezone.tzlookup.zonetree.CountryZoneUsage;
import com.android.libcore.timezone.util.Errors;
+import com.android.libcore.timezone.util.Errors.HaltExecutionException;
import com.ibm.icu.util.BasicTimeZone;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.GregorianCalendar;
@@ -29,6 +30,7 @@
import java.text.ParseException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -70,112 +72,171 @@
private final String countryZonesFile;
private final String zoneTabFile;
+ private final String backwardFile;
private final String outputFile;
/**
* Executes the generator.
- *
- * Positional arguments:
- * 1: The countryzones.txt file
- * 2: the zone.tab file
- * 3: the file to generate
*/
public static void main(String[] args) throws Exception {
- if (args.length != 3) {
+ if (args.length != 4) {
System.err.println(
"usage: java com.android.libcore.timezone.tzlookup.TzLookupGenerator"
- + " <input proto file> <zone.tab file> <output xml file>");
+ + " <input proto file> <zone.tab file> <backward file>"
+ + " <output xml file>");
System.exit(0);
}
- boolean success = new TzLookupGenerator(args[0], args[1], args[2]).execute();
+ boolean success = new TzLookupGenerator(args[0], args[1], args[2], args[3]).execute();
System.exit(success ? 0 : 1);
}
- TzLookupGenerator(String countryZonesFile, String zoneTabFile, String outputFile) {
+ TzLookupGenerator(String countryZonesFile, String zoneTabFile, String backwardFile,
+ String outputFile) {
this.countryZonesFile = countryZonesFile;
this.zoneTabFile = zoneTabFile;
+ this.backwardFile = backwardFile;
this.outputFile = outputFile;
}
- boolean execute() throws IOException {
- // Parse the countryzones input file.
- CountryZonesFile.CountryZones countryZonesIn;
+ boolean execute() {
+ Errors errors = new Errors();
try {
- countryZonesIn = CountryZonesFileSupport.parseCountryZonesTextFile(countryZonesFile);
- } catch (ParseException e) {
- logError("Unable to parse " + countryZonesFile, e);
- return false;
- }
+ // Parse the countryzones input file.
+ CountryZonesFile.CountryZones countryZonesIn =
+ parseAndValidateCountryZones(countryZonesFile, errors);
- // Check the countryzones rules version matches the version that ICU is using.
- String icuTzDataVersion = TimeZone.getTZDataVersion();
- String inputIanaVersion = countryZonesIn.getIanaVersion();
- if (!icuTzDataVersion.equals(inputIanaVersion)) {
- logError("Input data (countryzones.txt) is for " + inputIanaVersion
- + " but the ICU you have is for " + icuTzDataVersion);
- return false;
- }
+ // Check the countryzones.txt rules version matches the version that ICU is using.
+ String icuTzDataVersion = TimeZone.getTZDataVersion();
+ String inputIanaVersion = countryZonesIn.getIanaVersion();
+ if (!icuTzDataVersion.equals(inputIanaVersion)) {
+ throw errors.addFatalAndHalt("Input data (countryzones.txt) is for "
+ + inputIanaVersion + " but the ICU you have is for " + icuTzDataVersion);
+ }
- // Pull out information we want to validate against from zone.tab (which we have to assume
- // matches the ICU version since it doesn't contain its own version info).
- ZoneTabFile zoneTabIn = ZoneTabFile.parse(zoneTabFile);
- Map<String, List<String>> zoneTabMapping =
- ZoneTabFile.createCountryToOlsonIdsMap(zoneTabIn);
- List<CountryZonesFile.Country> countriesIn = countryZonesIn.getCountriesList();
- List<String> countriesInIsos = CountryZonesFileSupport.extractIsoCodes(countriesIn);
- // Sanity check the countryzones file only contains lower-case country codes. The output
- // file uses them and the on-device code assumes lower case.
- if (!Utils.allLowerCaseAscii(countriesInIsos)) {
- logError("Non-lowercase country ISO codes found in: " + countriesInIsos);
- return false;
- }
- // Sanity check the countryzones file doesn't contain duplicate country entries.
- if (!Utils.allUnique(countriesInIsos)) {
- logError("Duplicate input country entries found: " + countriesInIsos);
- return false;
- }
+ // Pull out information we want to validate against from zone.tab (which we have to
+ // assume matches the ICU version since it doesn't contain its own version info).
+ Map<String, List<String>> zoneTabMapping = parseZoneTabFile(zoneTabFile, errors);
- // Validate the country iso codes found in the countryzones against those in zone.tab.
- // zone.tab uses upper case, countryzones uses lower case.
- List<String> upperCaseCountriesInIsos = Utils.toUpperCase(countriesInIsos);
- Set<String> timezonesCountryIsos = new HashSet<>(upperCaseCountriesInIsos);
- Set<String> zoneTabCountryIsos = zoneTabMapping.keySet();
- if (!zoneTabCountryIsos.equals(timezonesCountryIsos)) {
- logError(zoneTabFile + " contains "
- + Utils.subtract(zoneTabCountryIsos, timezonesCountryIsos)
- + " not present in countryzones, "
- + countryZonesFile + " contains "
- + Utils.subtract(timezonesCountryIsos, zoneTabCountryIsos)
- + " not present in zonetab.");
- return false;
- }
+ List<CountryZonesFile.Country> countriesIn = countryZonesIn.getCountriesList();
+ List<String> countriesInIsos = CountryZonesFileSupport.extractIsoCodes(countriesIn);
- Errors processingErrors = new Errors();
- TzLookupFile.TimeZones timeZonesOut = createOutputTimeZones(
- inputIanaVersion, zoneTabMapping, countriesIn, processingErrors);
- if (!processingErrors.hasError()) {
+ // Sanity check the countryzones file only contains lower-case country codes. The output
+ // file uses them and the on-device code assumes lower case.
+ if (!Utils.allLowerCaseAscii(countriesInIsos)) {
+ throw errors.addFatalAndHalt(
+ "Non-lowercase country ISO codes found in: " + countriesInIsos);
+ }
+ // Sanity check the countryzones file doesn't contain duplicate country entries.
+ if (!Utils.allUnique(countriesInIsos)) {
+ throw errors.addFatalAndHalt(
+ "Duplicate input country entries found: " + countriesInIsos);
+ }
+
+ // Validate the country iso codes found in the countryzones.txt against those in
+ // zone.tab. zone.tab uses upper case, countryzones uses lower case.
+ List<String> upperCaseCountriesInIsos = Utils.toUpperCase(countriesInIsos);
+ Set<String> timezonesCountryIsos = new HashSet<>(upperCaseCountriesInIsos);
+ Set<String> zoneTabCountryIsos = zoneTabMapping.keySet();
+ if (!zoneTabCountryIsos.equals(timezonesCountryIsos)) {
+ throw errors.addFatalAndHalt(zoneTabFile + " contains "
+ + Utils.subtract(zoneTabCountryIsos, timezonesCountryIsos)
+ + " not present in countryzones, "
+ + countryZonesFile + " contains "
+ + Utils.subtract(timezonesCountryIsos, zoneTabCountryIsos)
+ + " not present in zonetab.");
+ }
+
+ // Obtain and validate a mapping from old IDs to new IDs.
+ Map<String, String> zoneIdLinks = parseAndValidateBackwardFile(backwardFile, errors);
+ errors.throwIfError("Errors accumulated");
+
+ TzLookupFile.TimeZones timeZonesOut = createOutputTimeZones(
+ inputIanaVersion, zoneTabMapping, countriesIn, zoneIdLinks, errors);
+ errors.throwIfError("Errors accumulated");
+
// Write the output structure if there wasn't an error.
logInfo("Writing " + outputFile);
try {
TzLookupFile.write(timeZonesOut, outputFile);
} catch (XMLStreamException e) {
- e.printStackTrace(System.err);
- processingErrors.addFatal("Unable to write output file");
+ throw errors.addFatalAndHalt("Unable to write output file", e);
+ }
+ return true;
+ } catch (HaltExecutionException | IOException e) {
+ logError("Stopping due to fatal condition", e);
+ return false;
+ } finally {
+ // Report all warnings / errors
+ if (!errors.isEmpty()) {
+ logInfo("Issues:\n" + errors.asString());
}
}
+ }
- // Report all warnings / errors
- if (!processingErrors.isEmpty()) {
- logInfo("Issues:\n" + processingErrors.asString());
+ private Map<String, List<String>> parseZoneTabFile(String zoneTabFile, Errors errors)
+ throws HaltExecutionException {
+ errors.pushScope("Parsing " + zoneTabFile);
+ try {
+ ZoneTabFile zoneTabIn;
+ zoneTabIn = ZoneTabFile.parse(zoneTabFile);
+ return ZoneTabFile.createCountryToOlsonIdsMap(zoneTabIn);
+ } catch (ParseException | IOException e) {
+ throw errors.addFatalAndHalt("Unable to parse " + zoneTabFile, e);
+ } finally {
+ errors.popScope();
}
+ }
- return !processingErrors.hasError();
+ /**
+ * Load the backward file and return the links contained within. This is used as the source of
+ * equivalent time zone IDs.
+ */
+ private static Map<String, String> parseAndValidateBackwardFile(
+ String backwardFile, Errors errors) {
+ errors.pushScope("Parsing " + backwardFile);
+ try {
+ BackwardFile backwardIn = BackwardFile.parse(backwardFile);
+
+ // Validate the links.
+ Map<String, String> zoneIdLinks = backwardIn.getDirectLinks();
+ zoneIdLinks.forEach(
+ (k, v) -> {
+ if (invalidTimeZoneId(k)) {
+ errors.addError("Bad 'from' link: " + k + "->" + v);
+ }
+ if (invalidTimeZoneId(v)) {
+ errors.addError("Bad 'to' link: " + k + "->" + v);
+ }
+ });
+ return zoneIdLinks;
+ } catch (ParseException | IOException e) {
+ errors.addError("Unable to parse " + backwardFile, e);
+ return null;
+ } finally {
+ errors.popScope();
+ }
+ }
+
+ private static CountryZonesFile.CountryZones parseAndValidateCountryZones(
+ String countryZonesFile, Errors errors) throws HaltExecutionException {
+ errors.pushScope("Parsing " + countryZonesFile);
+ try {
+ CountryZonesFile.CountryZones countryZonesIn;
+ countryZonesIn = CountryZonesFileSupport.parseCountryZonesTextFile(countryZonesFile);
+ return countryZonesIn;
+ } catch (ParseException | IOException e) {
+ throw errors.addFatalAndHalt("Unable to parse " + countryZonesFile, e);
+ } finally {
+ errors.popScope();
+ }
}
private static TzLookupFile.TimeZones createOutputTimeZones(String inputIanaVersion,
Map<String, List<String>> zoneTabMapping, List<CountryZonesFile.Country> countriesIn,
- Errors processingErrors) {
+ Map<String, String> zoneIdLinks, Errors errors)
+ throws HaltExecutionException {
+
// Start constructing the output structure.
TzLookupFile.TimeZones timeZonesOut = new TzLookupFile.TimeZones(inputIanaVersion);
TzLookupFile.CountryZones countryZonesOut = new TzLookupFile.CountryZones();
@@ -194,63 +255,61 @@
String isoCode = countryIn.getIsoCode();
List<String> zoneTabCountryTimeZoneIds = zoneTabMapping.get(isoCode.toUpperCase());
if (zoneTabCountryTimeZoneIds == null) {
- processingErrors.addError("Country=" + isoCode + " missing from zone.tab");
+ errors.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) {
+ zoneTabCountryTimeZoneIds, zoneIdLinks, errors);
+ if (countryOut == null) {
+ // Continue processing countries if there are only errors.
continue;
}
countryZonesOut.addCountry(countryOut);
}
+ errors.throwIfError("One or more countries failed");
return timeZonesOut;
}
private static TzLookupFile.Country processCountry(long offsetSampleTimeMillis,
long everUseUtcStartTimeMillis, CountryZonesFile.Country countryIn,
- List<String> zoneTabCountryTimeZoneIds,
- Errors processingErrors) {
+ List<String> zoneTabCountryTimeZoneIds, Map<String, String> zoneIdLinks,
+ Errors errors) {
String isoCode = countryIn.getIsoCode();
- processingErrors.pushScope("country=" + isoCode);
+ errors.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");
+ errors.addError("No time zones");
// No point in continuing.
return null;
}
- // Look for duplicate time zone IDs.
List<String> countryTimeZoneIds = CountryZonesFileSupport.extractIds(timeZonesIn);
+
+ // Look for duplicate time zone IDs.
if (!Utils.allUnique(countryTimeZoneIds)) {
- processingErrors.addError("country's zones=" + countryTimeZoneIds
- + " contains duplicates");
+ errors.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);
+ String defaultTimeZoneId = determineCountryDefaultZoneId(countryIn, errors);
if (defaultTimeZoneId == null) {
// No point in continuing.
return null;
}
boolean defaultTimeZoneBoost =
- determineCountryDefaultTimeZoneBoost(countryIn, processingErrors);
+ determineCountryDefaultTimeZoneBoost(countryIn, errors);
// Validate the default.
if (!countryTimeZoneIds.contains(defaultTimeZoneId)) {
- processingErrors.addError("defaultTimeZoneId=" + defaultTimeZoneId
+ errors.addError("defaultTimeZoneId=" + defaultTimeZoneId
+ " is not one of the country's zones=" + countryTimeZoneIds);
// No point in continuing.
return null;
@@ -258,52 +317,49 @@
// Validate the other zone IDs.
try {
- processingErrors.pushScope("validate country zone ids");
- boolean errors = false;
+ errors.pushScope("validate country zone ids");
for (String countryTimeZoneId : countryTimeZoneIds) {
if (invalidTimeZoneId(countryTimeZoneId)) {
- processingErrors.addError("countryTimeZoneId=" + countryTimeZoneId
+ errors.addError("countryTimeZoneId=" + countryTimeZoneId
+ " is not a valid zone ID");
- errors = true;
}
}
- if (errors) {
+ if (errors.hasError()) {
// No point in continuing.
return null;
}
} finally {
- processingErrors.popScope();
+ errors.popScope();
}
// 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");
+ errors.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);
+ errors.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);
+ // Validate the IDs being used against the IANA data for the country. If it fails
+ // the countryzones.txt needs to be updated with new IDs (or an alias can be added
+ // if there's some reason to keep using the old ID).
+ validateCountryZonesTzIdsAgainstIana(isoCode, zoneTabCountryTimeZoneIds,
+ timeZonesIn, zoneIdLinks, errors);
+ if (errors.hasError()) {
// No point in continuing.
return null;
}
} finally {
- processingErrors.popScope();
+ errors.popScope();
}
// Calculate countryZoneUsage.
- CountryZoneUsage countryZoneUsage =
- calculateCountryZoneUsage(countryIn, processingErrors);
+ CountryZoneUsage countryZoneUsage = calculateCountryZoneUsage(countryIn, errors);
if (countryZoneUsage == null) {
// No point in continuing with this country.
return null;
@@ -315,20 +371,18 @@
// Process each input time zone.
for (CountryZonesFile.TimeZoneMapping timeZoneIn : timeZonesIn) {
- processingErrors.pushScope(
+ errors.pushScope(
"id=" + timeZoneIn.getId() + ", offset=" + timeZoneIn.getUtcOffset()
+ ", shownInPicker=" + timeZoneIn.getShownInPicker());
try {
// Validate the offset information in countryIn.
- validateNonDstOffset(offsetSampleTimeMillis, countryIn, timeZoneIn,
- processingErrors);
+ validateNonDstOffset(offsetSampleTimeMillis, countryIn, timeZoneIn, errors);
String timeZoneInId = timeZoneIn.getId();
boolean shownInPicker = timeZoneIn.getShownInPicker();
if (!countryZoneUsage.hasEntry(timeZoneInId)) {
// This implies a programming error.
- processingErrors.addFatal(
- "No entry in CountryZoneUsage for " + timeZoneInId);
+ errors.addError("No entry in CountryZoneUsage for " + timeZoneInId);
return null;
}
@@ -343,13 +397,44 @@
timeZoneInId, shownInPicker, notUsedAfterInstant);
countryOut.addTimeZoneIdentifier(timeZoneIdOut);
} finally {
- processingErrors.popScope();
+ errors.popScope();
}
}
return countryOut;
} finally{
// End of country processing.
- processingErrors.popScope();
+ errors.popScope();
+ }
+ }
+
+ private static void validateCountryZonesTzIdsAgainstIana(String isoCode,
+ List<String> zoneTabCountryTimeZoneIds,
+ List<CountryZonesFile.TimeZoneMapping> timeZoneMappings,
+ Map<String, String> zoneIdLinks, Errors errors) {
+
+ List<String> expectedIanaTimeZoneIds = new ArrayList<>();
+ for (CountryZonesFile.TimeZoneMapping mapping : timeZoneMappings) {
+ String timeZoneId = mapping.getId();
+ String expectedIanaTimeZoneId;
+ if (!mapping.hasAliasId()) {
+ expectedIanaTimeZoneId = timeZoneId;
+ } else {
+ String aliasTimeZoneId = mapping.getAliasId();
+
+ // Confirm the alias is valid.
+ if (!aliasTimeZoneId.equals(zoneIdLinks.get(timeZoneId))) {
+ errors.addError(timeZoneId + " does not link to " + aliasTimeZoneId);
+ return;
+ }
+ expectedIanaTimeZoneId = aliasTimeZoneId;
+ }
+ expectedIanaTimeZoneIds.add(expectedIanaTimeZoneId);
+ }
+
+ if (!Utils.setEquals(zoneTabCountryTimeZoneIds, expectedIanaTimeZoneIds)) {
+ errors.addError("IANA lists " + isoCode
+ + " as having zones: " + zoneTabCountryTimeZoneIds
+ + ", but countryzones has " + expectedIanaTimeZoneIds);
}
}
@@ -357,20 +442,20 @@
* Determines the default zone ID for the country.
*/
private static String determineCountryDefaultZoneId(
- CountryZonesFile.Country countryIn, Errors processingErrorsOut) {
+ CountryZonesFile.Country countryIn, Errors errors) {
List<CountryZonesFile.TimeZoneMapping> timeZonesIn = countryIn.getTimeZoneMappingsList();
String defaultTimeZoneId;
if (countryIn.hasDefaultTimeZoneId()) {
defaultTimeZoneId = countryIn.getDefaultTimeZoneId();
if (invalidTimeZoneId(defaultTimeZoneId)) {
- processingErrorsOut.addError(
+ errors.addError(
"Default time zone ID " + defaultTimeZoneId + " is not valid");
// No point in continuing.
return null;
}
} else {
if (timeZonesIn.size() > 1) {
- processingErrorsOut.addError(
+ errors.addError(
"To pick a default time zone there must be a single offset group");
// No point in continuing.
return null;
@@ -384,14 +469,14 @@
* Determines the defaultTimeZoneBoost value for the country.
*/
private static boolean determineCountryDefaultTimeZoneBoost(
- CountryZonesFile.Country countryIn, Errors processingErrorsOut) {
+ CountryZonesFile.Country countryIn, Errors errors) {
if (!countryIn.hasDefaultTimeZoneBoost()) {
return false;
}
boolean defaultTimeZoneBoost = countryIn.getDefaultTimeZoneBoost();
if (!countryIn.hasDefaultTimeZoneId() && defaultTimeZoneBoost) {
- processingErrorsOut.addError(
+ errors.addError(
"defaultTimeZoneBoost is specified but defaultTimeZoneId is not explicit");
}
@@ -459,7 +544,7 @@
try {
utcOffsetMillis = Utils.parseUtcOffsetToMillis(utcOffsetString);
} catch (ParseException e) {
- errors.addFatal("Bad offset string: " + utcOffsetString);
+ errors.addError("Bad offset string: " + utcOffsetString);
return;
}
@@ -471,7 +556,7 @@
String timeZoneIdIn = timeZoneIn.getId();
if (invalidTimeZoneId(timeZoneIdIn)) {
- errors.addFatal("Time zone ID=" + timeZoneIdIn + " is not valid");
+ errors.addError("Time zone ID=" + timeZoneIdIn + " is not valid");
return;
}
@@ -481,7 +566,7 @@
timeZone.getOffset(offsetSampleTimeMillis, false /* local */, offsets);
int actualOffsetMillis = offsets[0];
if (actualOffsetMillis != utcOffsetMillis) {
- errors.addFatal("Offset mismatch: You will want to confirm the ordering for "
+ errors.addError("Offset mismatch: You will want to confirm the ordering for "
+ country.getIsoCode() + " still makes sense. Raw offset for "
+ timeZoneIdIn + " is " + Utils.toUtcOffsetString(actualOffsetMillis)
+ " and not " + Utils.toUtcOffsetString(utcOffsetMillis)
@@ -490,21 +575,20 @@
}
private static CountryZoneUsage calculateCountryZoneUsage(
- CountryZonesFile.Country countryIn, Errors processingErrors) {
- processingErrors.pushScope("Building zone tree");
+ CountryZonesFile.Country countryIn, Errors errors) {
+ errors.pushScope("Building zone tree");
try {
CountryZoneTree countryZoneTree = CountryZoneTree.create(
countryIn, ZONE_USAGE_CALCS_START, ZONE_USAGE_CALCS_END);
List<String> countryIssues = countryZoneTree.validateNoPriorityClashes();
if (!countryIssues.isEmpty()) {
- processingErrors
- .addError("Issues validating country zone trees. Adjust priorities:");
- countryIssues.forEach(processingErrors::addError);
+ errors.addError("Issues validating country zone trees. Adjust priorities:");
+ countryIssues.forEach(errors::addError);
return null;
}
return countryZoneTree.calculateCountryZoneUsage(ZONE_USAGE_NOT_AFTER_CUT_OFF);
} finally {
- processingErrors.popScope();
+ errors.popScope();
}
}
diff --git a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/ZoneTabFile.java b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/ZoneTabFile.java
index a939ca5..6b960f9 100644
--- a/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/ZoneTabFile.java
+++ b/input_tools/android/tzlookup_generator/src/main/java/com/android/libcore/timezone/tzlookup/ZoneTabFile.java
@@ -19,6 +19,7 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
+import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -36,7 +37,7 @@
private ZoneTabFile() {}
- static ZoneTabFile parse(String zoneTabFile) throws IOException {
+ static ZoneTabFile parse(String zoneTabFile) throws IOException, ParseException {
ZoneTabFile zoneTab = new ZoneTabFile();
List<String> lines = Files
@@ -49,9 +50,9 @@
.collect(Collectors.toList());
for (String mappingLine : mappingLines) {
- String[] fields = mappingLine.split("\t");
+ String[] fields = mappingLine.split("\t+");
if (fields.length < 3) {
- throw new IOException("Line is malformed: " + mappingLine);
+ throw new ParseException("Line is malformed: " + mappingLine, 0);
}
CountryEntry countryEntry = new CountryEntry(fields[0], fields[2]);
zoneTab.addCountryEntry(countryEntry);
@@ -74,7 +75,10 @@
countryEntry.isoCode, k -> new ArrayList<>());
olsonIds.add(countryEntry.olsonId);
}
- return countryIsoToOlsonIdsMap;
+ // Replace each list value with an immutable one.
+ countryIsoToOlsonIdsMap.forEach(
+ (k, v) -> countryIsoToOlsonIdsMap.put(k, Collections.unmodifiableList(v)));
+ return Collections.unmodifiableMap(countryIsoToOlsonIdsMap);
}
static class CountryEntry {
diff --git a/input_tools/android/tzlookup_generator/src/main/proto/country_zones_file.proto b/input_tools/android/tzlookup_generator/src/main/proto/country_zones_file.proto
index 6da1c29..9ed5644 100644
--- a/input_tools/android/tzlookup_generator/src/main/proto/country_zones_file.proto
+++ b/input_tools/android/tzlookup_generator/src/main/proto/country_zones_file.proto
@@ -35,7 +35,8 @@
message TimeZoneMapping {
required string id = 1;
- required string utcOffset = 2;
- optional bool shownInPicker = 3 [default = true];
- optional uint32 priority = 4 [default = 1];
+ optional string aliasId = 2;
+ required string utcOffset = 3;
+ optional bool shownInPicker = 4 [default = true];
+ optional uint32 priority = 5 [default = 1];
}
diff --git a/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/BackwardFileTest.java b/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/BackwardFileTest.java
new file mode 100644
index 0000000..c575a8e
--- /dev/null
+++ b/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/BackwardFileTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.libcore.timezone.tzlookup;
+
+import static junit.framework.TestCase.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.libcore.timezone.testing.TestUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.text.ParseException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class BackwardFileTest {
+
+ private Path tempDir;
+
+ @Before
+ public void setUp() throws Exception {
+ tempDir = Files.createTempDirectory("BackwardFileTest");
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ TestUtils.deleteDir(tempDir);
+ }
+
+ @Test
+ public void parseEmpty() throws Exception {
+ String file = createFile("");
+ BackwardFile backward = BackwardFile.parse(file);
+ assertTrue(backward.getDirectLinks().isEmpty());
+ }
+
+ @Test
+ public void parseIgnoresCommentsAndEmptyLines() throws Exception {
+ String file = createFile(
+ "# This is a comment",
+ "",
+ "# And another",
+ "Link\tAmerica/Nuuk\t\tAmerica/Godthab"
+ );
+ BackwardFile backward = BackwardFile.parse(file);
+
+ Map<String, String> expectedLinks = new HashMap<>();
+ expectedLinks.put("America/Godthab", "America/Nuuk");
+ assertEquals(expectedLinks, backward.getDirectLinks());
+ }
+
+ @Test
+ public void parse() throws Exception {
+ String file = createFile(
+ "# This is a comment",
+ "Link\tAmerica/Nuuk\t\tAmerica/Godthab",
+ "# This is a comment",
+ "Link\tAfrica/Nairobi\t\tAfrica/Asmera",
+ "# This is a comment",
+ "Link\tAfrica/Abidjan\t\tAfrica/Timbuktu",
+ "# This is a comment"
+ );
+ BackwardFile backward = BackwardFile.parse(file);
+ Map<String, String> expectedLinks = new HashMap<>();
+ expectedLinks.put("America/Godthab", "America/Nuuk");
+ expectedLinks.put("Africa/Asmera", "Africa/Nairobi");
+ expectedLinks.put("Africa/Timbuktu", "Africa/Abidjan");
+
+ assertEquals(expectedLinks, backward.getDirectLinks());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void getLinksWithLoop() throws Exception {
+ String file = createFile(
+ "Link\tAmerica/New_York\t\tAmerica/Los_Angeles",
+ "Link\tAmerica/Los_Angeles\t\tAmerica/Phoenix",
+ "Link\tAmerica/Phoenix\t\tAmerica/New_York"
+ );
+ BackwardFile backward = BackwardFile.parse(file);
+ backward.getDirectLinks();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void parseWithDupes() throws Exception {
+ String file = createFile(
+ "Link\tAmerica/New_York\t\tAmerica/Los_Angeles",
+ "Link\tAmerica/Phoenix\t\tAmerica/Los_Angeles"
+ );
+ BackwardFile.parse(file);
+ }
+
+ @Test(expected = ParseException.class)
+ public void parseMalformedFile() throws Exception {
+ // Mapping lines are expected to have at least three tab-separated columns.
+ String file = createFile("NotLink\tBooHoo");
+ BackwardFile.parse(file);
+ }
+
+ private String createFile(String... lines) throws IOException {
+ return TestUtils.createFile(tempDir, lines);
+ }
+}
diff --git a/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/TzLookupGeneratorTest.java b/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/TzLookupGeneratorTest.java
index 6a4843f..d8677cc 100644
--- a/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/TzLookupGeneratorTest.java
+++ b/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/TzLookupGeneratorTest.java
@@ -32,7 +32,9 @@
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.stream.Collectors;
import static com.android.libcore.timezone.testing.TestUtils.assertAbsent;
@@ -64,10 +66,11 @@
String countryZonesFile = createFile(tempDir, "THIS IS NOT A VALID FILE");
List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
+ String backwardFile = createBackwardFile(createValidBackwardLinks());
String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
TzLookupGenerator tzLookupGenerator =
- new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+ new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
assertFalse(tzLookupGenerator.execute());
}
@@ -82,11 +85,12 @@
List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
+ String backwardFile = createBackwardFile(createValidBackwardLinks());
String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
TzLookupGenerator tzLookupGenerator =
- new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+ new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
assertFalse(tzLookupGenerator.execute());
Path outputFilePath = Paths.get(outputFile);
@@ -100,13 +104,14 @@
createValidCountryGb().toBuilder().clearTimeZoneMappings().build();
CountryZonesFile.CountryZones countryZones = createValidCountryZones(gbWithoutZones);
String countryZonesFile = createCountryZonesFile(countryZones);
+ String backwardFile = createBackwardFile(createValidBackwardLinks());
String zoneTabFile = createZoneTabFile(createValidZoneTabEntriesGb());
String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
TzLookupGenerator tzLookupGenerator =
- new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+ new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
assertFalse(tzLookupGenerator.execute());
Path outputFilePath = Paths.get(outputFile);
@@ -125,13 +130,14 @@
CountryZonesFile.CountryZones countryZones =
createValidCountryZones(gbWithDuplicateZones);
String countryZonesFile = createCountryZonesFile(countryZones);
+ String backwardFile = createBackwardFile(createValidBackwardLinks());
String zoneTabFile = createZoneTabFile(createValidZoneTabEntriesGb());
String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
TzLookupGenerator tzLookupGenerator =
- new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+ new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
assertFalse(tzLookupGenerator.execute());
Path outputFilePath = Paths.get(outputFile);
@@ -150,11 +156,12 @@
List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
+ String backwardFile = createBackwardFile(createValidBackwardLinks());
String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
TzLookupGenerator tzLookupGenerator =
- new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+ new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
assertFalse(tzLookupGenerator.execute());
Path outputFilePath = Paths.get(outputFile);
@@ -172,11 +179,12 @@
List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
+ String backwardFile = createBackwardFile(createValidBackwardLinks());
String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
TzLookupGenerator tzLookupGenerator =
- new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+ new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
assertFalse(tzLookupGenerator.execute());
Path outputFilePath = Paths.get(outputFile);
@@ -194,7 +202,8 @@
.clearDefaultTimeZoneId().build();
List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
- String tzLookupXml = generateTzLookupXml(gbWithoutDefault, gbZoneTabEntries);
+ String tzLookupXml = generateTzLookupXml(gbWithoutDefault, gbZoneTabEntries,
+ createValidBackwardLinks());
// Check gb's time zone was defaulted.
assertContains(tzLookupXml, "code=\"gb\" default=\"" + gbTimeZoneId + "\"");
@@ -211,7 +220,8 @@
.build();
List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
- String tzLookupXml = generateTzLookupXml(gbWithExplicitDefaultTimeZone, gbZoneTabEntries);
+ String tzLookupXml = generateTzLookupXml(gbWithExplicitDefaultTimeZone, gbZoneTabEntries,
+ createValidBackwardLinks());
// Check gb's time zone was defaulted.
assertContains(tzLookupXml, "code=\"gb\" default=\"" + gbTimeZoneId + "\"");
@@ -229,11 +239,12 @@
List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
+ String backwardFile = createBackwardFile(createValidBackwardLinks());
String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
TzLookupGenerator tzLookupGenerator =
- new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+ new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
assertFalse(tzLookupGenerator.execute());
Path outputFilePath = Paths.get(outputFile);
@@ -251,11 +262,12 @@
List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
+ String backwardFile = createBackwardFile(createValidBackwardLinks());
String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
TzLookupGenerator tzLookupGenerator =
- new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+ new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
assertFalse(tzLookupGenerator.execute());
Path outputFilePath = Paths.get(outputFile);
@@ -271,11 +283,11 @@
String zoneTabFile =
createZoneTabFile(createValidZoneTabEntriesFr(), createValidZoneTabEntriesUs());
-
+ String backwardFile = createBackwardFile(createValidBackwardLinks());
String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
TzLookupGenerator tzLookupGenerator =
- new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+ new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
assertFalse(tzLookupGenerator.execute());
Path outputFilePath = Paths.get(outputFile);
@@ -293,11 +305,11 @@
String countryZonesFile = createCountryZonesFile(countryZones);
String zoneTabFile = createZoneTabFile(createValidZoneTabEntriesGb());
-
+ String backwardFile = createBackwardFile(createValidBackwardLinks());
String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
TzLookupGenerator tzLookupGenerator =
- new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+ new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
assertFalse(tzLookupGenerator.execute());
Path outputFilePath = Paths.get(outputFile);
@@ -313,10 +325,11 @@
String zoneTabFileWithDupes = createZoneTabFile(
createValidZoneTabEntriesGb(), createValidZoneTabEntriesGb());
+ String backwardFile = createBackwardFile(createValidBackwardLinks());
String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
- TzLookupGenerator tzLookupGenerator =
- new TzLookupGenerator(countryZonesFile, zoneTabFileWithDupes, outputFile);
+ TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
+ countryZonesFile, zoneTabFileWithDupes, backwardFile, outputFile);
assertFalse(tzLookupGenerator.execute());
Path outputFilePath = Paths.get(outputFile);
@@ -334,11 +347,12 @@
String countryZonesFile = createCountryZonesFile(countryZones);
String zoneTabFile = createZoneTabFile(createValidZoneTabEntriesGb());
+ String backwardFile = createBackwardFile(createValidBackwardLinks());
String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
TzLookupGenerator tzLookupGenerator =
- new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+ new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
assertFalse(tzLookupGenerator.execute());
Path outputFilePath = Paths.get(outputFile);
@@ -360,11 +374,12 @@
new ArrayList<>(createValidZoneTabEntriesGb());
zoneTabEntriesWithBadId.add(new ZoneTabFile.CountryEntry("GB", INVALID_TIME_ZONE_ID));
String zoneTabFile = createZoneTabFile(zoneTabEntriesWithBadId);
+ String backwardFile = createBackwardFile(createValidBackwardLinks());
String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
TzLookupGenerator tzLookupGenerator =
- new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+ new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
assertFalse(tzLookupGenerator.execute());
Path outputFilePath = Paths.get(outputFile);
@@ -372,9 +387,121 @@
}
@Test
+ public void badBackwardFile() throws Exception {
+ CountryZonesFile.CountryZones countryZones = createValidCountryZones(createValidCountryGb());
+ String countryZonesFile = createCountryZonesFile(countryZones);
+ String zoneTabFile = createZoneTabFile(createValidZoneTabEntriesGb());
+
+ String badBackwardFile = TestUtils.createFile(tempDir, "THIS IS NOT VALID");
+
+ String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
+
+ TzLookupGenerator tzLookupGenerator =
+ new TzLookupGenerator(countryZonesFile, zoneTabFile, badBackwardFile, outputFile);
+ assertFalse(tzLookupGenerator.execute());
+
+ Path outputFilePath = Paths.get(outputFile);
+ assertEquals(0, Files.size(outputFilePath));
+ }
+
+ @Test
+ public void usingOldLinksValid() throws Exception {
+ // This simulates a case where America/Godthab has been superseded by America/Nuuk in IANA
+ // data, but Android wants to continue using America/Godthab.
+ String countryZonesWithOldIdText =
+ "isoCode:\"gl\"\n"
+ + "defaultTimeZoneId:\"America/Godthab\"\n"
+ + "timeZoneMappings:<\n"
+ + " utcOffset:\"0:00\"\n"
+ + " id:\"America/Danmarkshavn\"\n"
+ + ">\n"
+ + "\n"
+ + "timeZoneMappings:<\n"
+ + " utcOffset:\"-1:00\"\n"
+ + " id:\"America/Scoresbysund\"\n"
+ + ">\n"
+ + "\n"
+ + "timeZoneMappings:<\n"
+ + " utcOffset:\"-3:00\"\n"
+ + " id:\"America/Godthab\"\n"
+ + " aliasId:\"America/Nuuk\"\n"
+ + ">\n"
+ + "\n"
+ + "timeZoneMappings:<\n"
+ + " utcOffset:\"-4:00\"\n"
+ + " id:\"America/Thule\"\n"
+ + ">\n";
+ Country country = parseCountry(countryZonesWithOldIdText);
+ List<ZoneTabFile.CountryEntry> zoneTabWithNewIds = Arrays.asList(
+ new ZoneTabFile.CountryEntry("GL", "America/Nuuk"),
+ new ZoneTabFile.CountryEntry("GL", "America/Danmarkshavn"),
+ new ZoneTabFile.CountryEntry("GL", "America/Scoresbysund"),
+ new ZoneTabFile.CountryEntry("GL", "America/Thule")
+ );
+ Map<String, String> links = new HashMap<>();
+ links.put("America/Godthab", "America/Nuuk");
+
+ String tzLookupXml = generateTzLookupXml(country, zoneTabWithNewIds, links);
+
+ String expectedOutput =
+ "<id>America/Danmarkshavn</id>\n"
+ + "<id>America/Scoresbysund</id>\n"
+ + "<id>America/Godthab</id>\n"
+ + "<id>America/Thule</id>\n";
+ String[] expectedLines = expectedOutput.split("\\n");
+ for (String expectedLine : expectedLines) {
+ assertContains(tzLookupXml, expectedLine);
+ }
+ }
+
+ @Test
+ public void usingOldLinksMissingAlias() throws Exception {
+ // This simulates a case where America/Godthab has been superseded by America/Nuuk in IANA
+ // data, but the Android file hasn't been updated properly.
+ String countryZonesWithOldIdText =
+ "isoCode:\"gl\"\n"
+ + "defaultTimeZoneId:\"America/Godthab\"\n"
+ + "timeZoneMappings:<\n"
+ + " utcOffset:\"0:00\"\n"
+ + " id:\"America/Danmarkshavn\"\n"
+ + ">\n"
+ + "\n"
+ + "timeZoneMappings:<\n"
+ + " utcOffset:\"-1:00\"\n"
+ + " id:\"America/Scoresbysund\"\n"
+ + ">\n"
+ + "\n"
+ + "timeZoneMappings:<\n"
+ + " utcOffset:\"-3:00\"\n"
+ + " id:\"America/Godthab\"\n"
+
+ // Exclude the crucial line that tells the generator we meant to use an old ID...
+ /* + " aliasId:\"America/Nuuk\"\n" */
+
+ + ">\n"
+ + "\n"
+ + "timeZoneMappings:<\n"
+ + " utcOffset:\"-4:00\"\n"
+ + " id:\"America/Thule\"\n"
+ + ">\n";
+ Country country = parseCountry(countryZonesWithOldIdText);
+ List<ZoneTabFile.CountryEntry> zoneTabWithNewIds = Arrays.asList(
+ new ZoneTabFile.CountryEntry("GL", "America/Nuuk"),
+ new ZoneTabFile.CountryEntry("GL", "America/Danmarkshavn"),
+ new ZoneTabFile.CountryEntry("GL", "America/Scoresbysund"),
+ new ZoneTabFile.CountryEntry("GL", "America/Thule")
+ );
+ Map<String, String> links = new HashMap<>();
+ links.put("America/Godthab", "America/Nuuk");
+
+ generateTzLookupXmlExpectFailure(country, zoneTabWithNewIds, links);
+ }
+
+ @Test
public void everUtc_true() throws Exception {
CountryZonesFile.Country validCountryGb = createValidCountryGb();
- String tzLookupXml = generateTzLookupXml(validCountryGb, createValidZoneTabEntriesGb());
+ String tzLookupXml = generateTzLookupXml(validCountryGb, createValidZoneTabEntriesGb(),
+ createValidBackwardLinks());
// Check gb's entry contains everutc="y".
assertContains(tzLookupXml, "everutc=\"y\"");
@@ -383,7 +510,8 @@
@Test
public void everUtc_false() throws Exception {
CountryZonesFile.Country validCountryFr = createValidCountryFr();
- String tzLookupXml = generateTzLookupXml(validCountryFr, createValidZoneTabEntriesFr());
+ String tzLookupXml = generateTzLookupXml(validCountryFr, createValidZoneTabEntriesFr(),
+ createValidBackwardLinks());
// Check fr's entry contains everutc="n".
assertContains(tzLookupXml, "everutc=\"n\"");
@@ -401,7 +529,8 @@
countryBuilder.setTimeZoneMappings(0, timeZoneMappingBuilder);
CountryZonesFile.Country country = countryBuilder.build();
- String tzLookupXml = generateTzLookupXml(country, createValidZoneTabEntriesFr());
+ String tzLookupXml = generateTzLookupXml(country, createValidZoneTabEntriesFr(),
+ createValidBackwardLinks());
assertContains(tzLookupXml, "picker=\"n\"");
}
@@ -418,7 +547,8 @@
countryBuilder.setTimeZoneMappings(0, timeZoneMappingBuilder);
CountryZonesFile.Country country = countryBuilder.build();
- String tzLookupXml = generateTzLookupXml(country, createValidZoneTabEntriesFr());
+ String tzLookupXml = generateTzLookupXml(country, createValidZoneTabEntriesFr(),
+ createValidBackwardLinks());
// We should not see anything "picker="y" is the implicit default.
assertAbsent(tzLookupXml, "picker=");
@@ -428,7 +558,8 @@
public void notAfter() throws Exception {
CountryZonesFile.Country country = createValidCountryUs();
List<ZoneTabFile.CountryEntry> zoneTabEntries = createValidZoneTabEntriesUs();
- String tzLookupXml = generateTzLookupXml(country, zoneTabEntries);
+ String tzLookupXml = generateTzLookupXml(country, zoneTabEntries,
+ createValidBackwardLinks());
String expectedOutput =
"<id>America/New_York</id>\n"
+ "<id notafter=\"167814000000\">America/Detroit</id>\n"
@@ -466,17 +597,19 @@
}
private String generateTzLookupXml(CountryZonesFile.Country country,
- List<ZoneTabFile.CountryEntry> zoneTabEntries) throws Exception {
+ List<ZoneTabFile.CountryEntry> zoneTabEntries, Map<String, String> backwardLinks)
+ throws Exception {
CountryZonesFile.CountryZones countryZones = createValidCountryZones(country);
String countryZonesFile = createCountryZonesFile(countryZones);
String zoneTabFile = createZoneTabFile(zoneTabEntries);
+ String backwardFile = createBackwardFile(backwardLinks);
String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
TzLookupGenerator tzLookupGenerator =
- new TzLookupGenerator(countryZonesFile, zoneTabFile, outputFile);
+ new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
assertTrue(tzLookupGenerator.execute());
Path outputFilePath = Paths.get(outputFile);
@@ -485,6 +618,23 @@
return readFileToString(outputFilePath);
}
+ private void generateTzLookupXmlExpectFailure(CountryZonesFile.Country country,
+ List<ZoneTabFile.CountryEntry> zoneTabEntries, Map<String, String> backwardLinks)
+ throws Exception {
+
+ CountryZonesFile.CountryZones countryZones = createValidCountryZones(country);
+ String countryZonesFile = createCountryZonesFile(countryZones);
+
+ String zoneTabFile = createZoneTabFile(zoneTabEntries);
+ String backwardFile = createBackwardFile(backwardLinks);
+
+ String outputFile = Files.createTempFile(tempDir, "out", null /* suffix */).toString();
+
+ TzLookupGenerator tzLookupGenerator =
+ new TzLookupGenerator(countryZonesFile, zoneTabFile, backwardFile, outputFile);
+ assertFalse(tzLookupGenerator.execute());
+ }
+
private static String readFileToString(Path file) throws IOException {
return new String(Files.readAllBytes(file), StandardCharsets.UTF_8);
}
@@ -709,6 +859,19 @@
new ZoneTabFile.CountryEntry("FR", "Europe/Paris"));
}
+ private String createBackwardFile(Map<String, String> links) throws Exception {
+ List<String> lines = links.entrySet().stream()
+ .map(x -> "Link\t" + x.getValue() + "\t\t" + x.getKey())
+ .collect(Collectors.toList());
+ return TestUtils.createFile(tempDir, lines.toArray(new String[0]));
+ }
+
+ private static Map<String, String> createValidBackwardLinks() {
+ Map<String, String> map = new HashMap<>();
+ map.put("America/Godthab", "America/Nuuk");
+ return map;
+ }
+
private static Country parseCountry(String text) throws Exception {
Country.Builder builder = Country.newBuilder();
TextFormat.getParser().merge(text, builder);
diff --git a/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/ZoneTabFileTest.java b/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/ZoneTabFileTest.java
index b581ffd..108b902 100644
--- a/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/ZoneTabFileTest.java
+++ b/input_tools/android/tzlookup_generator/src/test/java/com/android/libcore/timezone/tzlookup/ZoneTabFileTest.java
@@ -25,6 +25,7 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.text.ParseException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -32,7 +33,6 @@
import static junit.framework.TestCase.assertEquals;
import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
public class ZoneTabFileTest {
@@ -75,7 +75,7 @@
"# This is a comment",
"GB\tStuff\tEurope/London\tStuff",
"# This is a comment",
- "US\tStuff\tAmerica/New_York\tStuff",
+ "US\tStuff\t\tAmerica/New_York\tStuff",
"# This is a comment",
"US\tStuff\tAmerica/Los_Angeles",
"# This is a comment"
@@ -89,14 +89,11 @@
zoneTab.getCountryEntries());
}
- @Test
+ @Test(expected = ParseException.class)
public void parseMalformedFile() throws Exception {
// Mapping lines are expected to have at least three tab-separated columns.
String file = createFile("GB\tStuff");
- try {
- ZoneTabFile.parse(file);
- fail();
- } catch (IOException expected) {}
+ ZoneTabFile.parse(file);
}
@Test
diff --git a/update-tzdata.py b/update-tzdata.py
index 61ab30e..ee370c1 100755
--- a/update-tzdata.py
+++ b/update-tzdata.py
@@ -184,8 +184,10 @@
tzdatautil.InvokeSoong(android_build_top, ['tzlookup_generator'])
zone_tab_file = '%s/zone.tab' % iana_data_dir
+ backward_file = '%s/backward' % iana_data_dir
command = '%s/bin/tzlookup_generator' % android_host_out
- subprocess.check_call([command, countryzones_source_file, zone_tab_file, tzlookup_dest_file])
+ subprocess.check_call([command, countryzones_source_file, zone_tab_file, backward_file,
+ tzlookup_dest_file])
def BuildTelephonylookup():