554 lines
23 KiB
Java
554 lines
23 KiB
Java
/*
|
|
* Copyright (C) 2017 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.i18n.timezone;
|
|
|
|
import static com.android.i18n.timezone.XmlUtils.checkOnEndTag;
|
|
import static com.android.i18n.timezone.XmlUtils.consumeText;
|
|
import static com.android.i18n.timezone.XmlUtils.consumeUntilEndTag;
|
|
import static com.android.i18n.timezone.XmlUtils.findNextStartTagOrEndTagNoRecurse;
|
|
import static com.android.i18n.timezone.XmlUtils.findNextStartTagOrThrowNoRecurse;
|
|
import static com.android.i18n.timezone.XmlUtils.normalizeCountryIso;
|
|
import static com.android.i18n.timezone.XmlUtils.parseBooleanAttribute;
|
|
import static com.android.i18n.timezone.XmlUtils.parseLongAttribute;
|
|
import static com.android.i18n.timezone.XmlUtils.parseStringListAttribute;
|
|
|
|
import org.xmlpull.v1.XmlPullParser;
|
|
import org.xmlpull.v1.XmlPullParserException;
|
|
import org.xmlpull.v1.XmlPullParserFactory;
|
|
|
|
import java.io.IOException;
|
|
import java.io.Reader;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collections;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
|
|
import com.android.i18n.timezone.CountryTimeZones.TimeZoneMapping;
|
|
import com.android.i18n.timezone.XmlUtils.ReaderSupplier;
|
|
import com.android.i18n.util.Log;
|
|
|
|
/**
|
|
* A class that can find matching time zones by loading data from the tzlookup.xml file.
|
|
* @hide
|
|
*/
|
|
@libcore.api.CorePlatformApi
|
|
public final class TimeZoneFinder {
|
|
|
|
public static final String TZLOOKUP_FILE_NAME = "tzlookup.xml";
|
|
|
|
// Root element. e.g. <timezones ianaversion="2017b">
|
|
private static final String TIMEZONES_ELEMENT = "timezones";
|
|
private static final String IANA_VERSION_ATTRIBUTE = "ianaversion";
|
|
|
|
// Country zones section. e.g. <countryzones>
|
|
private static final String COUNTRY_ZONES_ELEMENT = "countryzones";
|
|
|
|
// Country data. e.g.
|
|
// <country code="gb" default="Europe/London" defaultBoost="y" everutc="y">
|
|
private static final String COUNTRY_ELEMENT = "country";
|
|
private static final String COUNTRY_CODE_ATTRIBUTE = "code";
|
|
private static final String DEFAULT_TIME_ZONE_ID_ATTRIBUTE = "default";
|
|
private static final String DEFAULT_TIME_ZONE_BOOST_ATTRIBUTE = "defaultBoost";
|
|
private static final String EVER_USES_UTC_ATTRIBUTE = "everutc";
|
|
|
|
// Country -> Time zone mapping. e.g. <id>ZoneId</id>, <id picker="n">ZoneId</id>,
|
|
// <id notafter={timestamp} alts="{alternative ids}">ZoneId</id>
|
|
// The default for the picker attribute when unspecified is "y".
|
|
// The notafter attribute is optional. It specifies a timestamp (time in milliseconds from Unix
|
|
// epoch start) after which the zone is not (effectively) in use. If unspecified the zone is in
|
|
// use forever.
|
|
// The alts attribute is optional. It contains a comma-separated String of alternative IDs that
|
|
// are exact synonyms for the ZoneId.
|
|
private static final String ZONE_ID_ELEMENT = "id";
|
|
private static final String ZONE_SHOW_IN_PICKER_ATTRIBUTE = "picker";
|
|
private static final String ZONE_NOT_USED_AFTER_ATTRIBUTE = "notafter";
|
|
private static final String ZONE_ALTERNATIVE_IDS_ATTRIBUTE = "alts";
|
|
|
|
private static TimeZoneFinder instance;
|
|
|
|
private final ReaderSupplier xmlSource;
|
|
|
|
// Cached field for the last country looked up.
|
|
private CountryTimeZones lastCountryTimeZones;
|
|
|
|
private TimeZoneFinder(ReaderSupplier xmlSource) {
|
|
this.xmlSource = xmlSource;
|
|
}
|
|
|
|
/**
|
|
* Obtains an instance for use when resolving time zones. This method handles using the correct
|
|
* file when there are several to choose from. This method never returns {@code null}. No
|
|
* in-depth validation is performed on the file content, see {@link #validate()}.
|
|
*/
|
|
@libcore.api.CorePlatformApi
|
|
public static TimeZoneFinder getInstance() {
|
|
synchronized(TimeZoneFinder.class) {
|
|
if (instance == null) {
|
|
String[] tzLookupFilePaths =
|
|
TimeZoneDataFiles.getTimeZoneFilePaths(TZLOOKUP_FILE_NAME);
|
|
instance = createInstanceWithFallback(tzLookupFilePaths);
|
|
}
|
|
}
|
|
return instance;
|
|
}
|
|
|
|
// VisibleForTesting
|
|
public static TimeZoneFinder createInstanceWithFallback(String... tzLookupFilePaths) {
|
|
IOException lastException = null;
|
|
for (String tzLookupFilePath : tzLookupFilePaths) {
|
|
try {
|
|
// We assume that any file in /data was validated before install, and the system
|
|
// file was validated before the device shipped. Therefore, we do not pay the
|
|
// validation cost here.
|
|
return createInstance(tzLookupFilePath);
|
|
} catch (IOException e) {
|
|
// There's expected to be two files, and it's normal for the first file not to
|
|
// exist so we don't log, but keep the lastException so we can log it if there
|
|
// are no valid files available.
|
|
if (lastException != null) {
|
|
e.addSuppressed(lastException);
|
|
}
|
|
lastException = e;
|
|
}
|
|
}
|
|
|
|
Log.e("No valid file found in set: " + Arrays.toString(tzLookupFilePaths)
|
|
+ " Printing exceptions and falling back to empty map.", lastException);
|
|
return createInstanceForTests("<timezones><countryzones /></timezones>");
|
|
}
|
|
|
|
/**
|
|
* Obtains an instance using a specific data file, throwing an IOException if the file does not
|
|
* exist or is not readable. This method never returns {@code null}. No in-depth validation is
|
|
* performed on the file content, see {@link #validate()}.
|
|
*/
|
|
@libcore.api.CorePlatformApi
|
|
public static TimeZoneFinder createInstance(String path) throws IOException {
|
|
ReaderSupplier xmlSupplier = ReaderSupplier.forFile(path, StandardCharsets.UTF_8);
|
|
return new TimeZoneFinder(xmlSupplier);
|
|
}
|
|
|
|
/** Used to create an instance using an in-memory XML String instead of a file. */
|
|
// VisibleForTesting
|
|
public static TimeZoneFinder createInstanceForTests(String xml) {
|
|
return new TimeZoneFinder(ReaderSupplier.forString(xml));
|
|
}
|
|
|
|
/**
|
|
* Parses the data file, throws an exception if it is invalid or cannot be read.
|
|
*/
|
|
@libcore.api.CorePlatformApi
|
|
public void validate() throws IOException {
|
|
try {
|
|
processXml(new TimeZonesValidator());
|
|
} catch (XmlPullParserException e) {
|
|
throw new IOException("Parsing error", e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the IANA rules version associated with the data. If there is no version information
|
|
* or there is a problem reading the file then {@code null} is returned.
|
|
*/
|
|
@libcore.api.CorePlatformApi
|
|
public String getIanaVersion() {
|
|
IanaVersionExtractor ianaVersionExtractor = new IanaVersionExtractor();
|
|
try {
|
|
processXml(ianaVersionExtractor);
|
|
return ianaVersionExtractor.getIanaVersion();
|
|
} catch (XmlPullParserException | IOException e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads all the country <-> time zone mapping data into memory. This method can return
|
|
* {@code null} in the event of an error while reading the underlying data files.
|
|
*/
|
|
@libcore.api.CorePlatformApi
|
|
public CountryZonesFinder getCountryZonesFinder() {
|
|
CountryZonesLookupExtractor extractor = new CountryZonesLookupExtractor();
|
|
try {
|
|
processXml(extractor);
|
|
|
|
return extractor.getCountryZonesLookup();
|
|
} catch (XmlPullParserException | IOException e) {
|
|
Log.w("Error reading country zones ", e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a {@link CountryTimeZones} object associated with the specified country code.
|
|
* Caching is handled as needed. If the country code is not recognized or there is an error
|
|
* during lookup this method can return null.
|
|
*/
|
|
@libcore.api.CorePlatformApi
|
|
public CountryTimeZones lookupCountryTimeZones(String countryIso) {
|
|
synchronized (this) {
|
|
if (lastCountryTimeZones != null
|
|
&& lastCountryTimeZones.matchesCountryCode(countryIso)) {
|
|
return lastCountryTimeZones;
|
|
}
|
|
}
|
|
|
|
SelectiveCountryTimeZonesExtractor extractor =
|
|
new SelectiveCountryTimeZonesExtractor(countryIso);
|
|
try {
|
|
processXml(extractor);
|
|
|
|
CountryTimeZones countryTimeZones = extractor.getValidatedCountryTimeZones();
|
|
if (countryTimeZones == null) {
|
|
// None matched. Return the null but don't change the cached value.
|
|
return null;
|
|
}
|
|
|
|
// Update the cached value.
|
|
synchronized (this) {
|
|
lastCountryTimeZones = countryTimeZones;
|
|
}
|
|
return countryTimeZones;
|
|
} catch (XmlPullParserException | IOException e) {
|
|
Log.w("Error reading country zones ", e);
|
|
|
|
// Error - don't change the cached value.
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes the XML, applying the {@link TimeZonesProcessor} to the <countryzones>
|
|
* element. Processing can terminate early if the {@link TimeZonesProcessor#processCountryZones(
|
|
* String, String, boolean, boolean, List, String)} returns {@link TimeZonesProcessor#HALT} or
|
|
* it throws an exception.
|
|
*/
|
|
private void processXml(TimeZonesProcessor processor)
|
|
throws XmlPullParserException, IOException {
|
|
try (Reader reader = xmlSource.get()) {
|
|
XmlPullParserFactory xmlPullParserFactory = XmlPullParserFactory.newInstance();
|
|
xmlPullParserFactory.setNamespaceAware(false);
|
|
|
|
XmlPullParser parser = xmlPullParserFactory.newPullParser();
|
|
parser.setInput(reader);
|
|
|
|
/*
|
|
* The expected XML structure is:
|
|
* <timezones ianaversion="2017b">
|
|
* <countryzones>
|
|
* <country code="us" default="America/New_York">
|
|
* <id>America/New_York"</id>
|
|
* ...
|
|
* <id picker="n">America/Indiana/Vincennes</id>
|
|
* ...
|
|
* <id>America/Los_Angeles</id>
|
|
* </country>
|
|
* <country code="gb" default="Europe/London" defaultBoost="y">
|
|
* <id>Europe/London</id>
|
|
* </country>
|
|
* </countryzones>
|
|
* </timezones>
|
|
*/
|
|
|
|
findNextStartTagOrThrowNoRecurse(parser, TIMEZONES_ELEMENT);
|
|
|
|
// We do not require the ianaversion attribute be present. It is metadata that helps
|
|
// with versioning but is not required.
|
|
String ianaVersion = parser.getAttributeValue(
|
|
null /* namespace */, IANA_VERSION_ATTRIBUTE);
|
|
if (processor.processHeader(ianaVersion) == TimeZonesProcessor.HALT) {
|
|
return;
|
|
}
|
|
|
|
// There is only one expected sub-element <countryzones> in the format currently, skip
|
|
// over anything before it.
|
|
findNextStartTagOrThrowNoRecurse(parser, COUNTRY_ZONES_ELEMENT);
|
|
|
|
if (processCountryZones(parser, processor) == TimeZonesProcessor.HALT) {
|
|
return;
|
|
}
|
|
|
|
// Make sure we are on the </countryzones> tag.
|
|
checkOnEndTag(parser, COUNTRY_ZONES_ELEMENT);
|
|
|
|
// Advance to the next tag.
|
|
parser.next();
|
|
|
|
// Skip anything until </timezones>, and make sure the file is not truncated and we can
|
|
// find the end.
|
|
consumeUntilEndTag(parser, TIMEZONES_ELEMENT);
|
|
|
|
// Make sure we are on the </timezones> tag.
|
|
checkOnEndTag(parser, TIMEZONES_ELEMENT);
|
|
}
|
|
}
|
|
|
|
private static boolean processCountryZones(XmlPullParser parser,
|
|
TimeZonesProcessor processor) throws IOException, XmlPullParserException {
|
|
|
|
// Skip over any unexpected elements and process <country> elements.
|
|
while (findNextStartTagOrEndTagNoRecurse(parser, COUNTRY_ELEMENT)) {
|
|
String code = parser.getAttributeValue(
|
|
null /* namespace */, COUNTRY_CODE_ATTRIBUTE);
|
|
if (code == null || code.isEmpty()) {
|
|
throw new XmlPullParserException(
|
|
"Unable to find country code: " + parser.getPositionDescription());
|
|
}
|
|
|
|
String defaultTimeZoneId = parser.getAttributeValue(
|
|
null /* namespace */, DEFAULT_TIME_ZONE_ID_ATTRIBUTE);
|
|
if (defaultTimeZoneId == null || defaultTimeZoneId.isEmpty()) {
|
|
throw new XmlPullParserException("Unable to find default time zone ID: "
|
|
+ parser.getPositionDescription());
|
|
}
|
|
|
|
boolean defaultTimeZoneBoost = parseBooleanAttribute(parser,
|
|
DEFAULT_TIME_ZONE_BOOST_ATTRIBUTE, false);
|
|
|
|
Boolean everUsesUtc = parseBooleanAttribute(
|
|
parser, EVER_USES_UTC_ATTRIBUTE, null /* defaultValue */);
|
|
if (everUsesUtc == null) {
|
|
// There is no valid default: we require this to be specified.
|
|
throw new XmlPullParserException(
|
|
"Unable to find UTC hint attribute (" + EVER_USES_UTC_ATTRIBUTE + "): "
|
|
+ parser.getPositionDescription());
|
|
}
|
|
|
|
String debugInfo = parser.getPositionDescription();
|
|
List<TimeZoneMapping> timeZoneMappings = parseTimeZoneMappings(parser);
|
|
boolean result = processor.processCountryZones(code, defaultTimeZoneId,
|
|
defaultTimeZoneBoost, everUsesUtc, timeZoneMappings, debugInfo);
|
|
if (result == TimeZonesProcessor.HALT) {
|
|
return TimeZonesProcessor.HALT;
|
|
}
|
|
|
|
// Make sure we are on the </country> element.
|
|
checkOnEndTag(parser, COUNTRY_ELEMENT);
|
|
}
|
|
|
|
return TimeZonesProcessor.CONTINUE;
|
|
}
|
|
|
|
private static List<TimeZoneMapping> parseTimeZoneMappings(XmlPullParser parser)
|
|
throws IOException, XmlPullParserException {
|
|
List<TimeZoneMapping> timeZoneMappings = new ArrayList<>();
|
|
|
|
// Skip over any unexpected elements and process <id> elements.
|
|
while (findNextStartTagOrEndTagNoRecurse(parser, ZONE_ID_ELEMENT)) {
|
|
// The picker attribute is optional and defaulted to true.
|
|
boolean showInPicker = parseBooleanAttribute(
|
|
parser, ZONE_SHOW_IN_PICKER_ATTRIBUTE, true /* defaultValue */);
|
|
Long notUsedAfter = parseLongAttribute(
|
|
parser, ZONE_NOT_USED_AFTER_ATTRIBUTE, null /* defaultValue */);
|
|
List<String> alternativeIds = parseStringListAttribute(
|
|
parser, ZONE_ALTERNATIVE_IDS_ATTRIBUTE, Collections.emptyList());
|
|
String zoneIdString = consumeText(parser);
|
|
|
|
// Make sure we are on the </id> element.
|
|
checkOnEndTag(parser, ZONE_ID_ELEMENT);
|
|
|
|
// Process the TimeZoneMapping.
|
|
if (zoneIdString == null || zoneIdString.length() == 0) {
|
|
throw new XmlPullParserException("Missing text for " + ZONE_ID_ELEMENT + "): "
|
|
+ parser.getPositionDescription());
|
|
}
|
|
|
|
// intern() zone Ids because they are a fixed set of well-known strings that are used in
|
|
// other low-level library calls.
|
|
String internedZoneIdString = zoneIdString.intern();
|
|
List<String> internedAlternativeIds = internStrings(alternativeIds);
|
|
|
|
TimeZoneMapping timeZoneMapping = new TimeZoneMapping(
|
|
internedZoneIdString, showInPicker, notUsedAfter, internedAlternativeIds);
|
|
timeZoneMappings.add(timeZoneMapping);
|
|
}
|
|
|
|
// The list is made unmodifiable to avoid callers changing it.
|
|
return Collections.unmodifiableList(timeZoneMappings);
|
|
}
|
|
|
|
private static List<String> internStrings(List<String> stringsToIntern) {
|
|
if (stringsToIntern.isEmpty()) {
|
|
return stringsToIntern;
|
|
}
|
|
|
|
List<String> internedStrings = new ArrayList<>(stringsToIntern.size());
|
|
for (String stringToIntern : stringsToIntern) {
|
|
internedStrings.add(stringToIntern.intern());
|
|
}
|
|
return internedStrings;
|
|
}
|
|
|
|
/**
|
|
* Processes <timezones> data.
|
|
*/
|
|
private interface TimeZonesProcessor {
|
|
|
|
boolean CONTINUE = true;
|
|
boolean HALT = false;
|
|
|
|
/**
|
|
* Return {@link #CONTINUE} if processing of the XML should continue, {@link #HALT} if it
|
|
* should stop (but without considering this an error). Problems with the data are
|
|
* reported as an exception.
|
|
*
|
|
* <p>The default implementation returns {@link #CONTINUE}.
|
|
*/
|
|
default boolean processHeader(String ianaVersion) throws XmlPullParserException {
|
|
return CONTINUE;
|
|
}
|
|
|
|
/**
|
|
* Returns {@link #CONTINUE} if processing of the XML should continue, {@link #HALT} if it
|
|
* should stop (but without considering this an error). Problems with the data are
|
|
* reported as an exception.
|
|
*
|
|
* <p>The default implementation returns {@link #CONTINUE}.
|
|
*/
|
|
default boolean processCountryZones(String countryIso, String defaultTimeZoneId,
|
|
boolean defaultTimeZoneBoost, boolean everUsesUtc,
|
|
List<TimeZoneMapping> timeZoneMappings, String debugInfo)
|
|
throws XmlPullParserException {
|
|
return CONTINUE;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates <countryzones> elements. Intended to be used before a proposed installation
|
|
* of new data. To be valid the country ISO code must be normalized, unique, the default time
|
|
* zone ID must be one of the time zones IDs and the time zone IDs list must not be empty. The
|
|
* IDs themselves are not checked against other data to see if they are recognized because other
|
|
* classes will not have been updated with the associated new time zone data yet and so will not
|
|
* be aware of newly added IDs.
|
|
*/
|
|
private static class TimeZonesValidator implements TimeZonesProcessor {
|
|
|
|
private final Set<String> knownCountryCodes = new HashSet<>();
|
|
|
|
@Override
|
|
public boolean processCountryZones(String countryIso, String defaultTimeZoneId,
|
|
boolean defaultTimeZoneBoost, boolean everUsesUtc,
|
|
List<TimeZoneMapping> timeZoneMappings, String debugInfo)
|
|
throws XmlPullParserException {
|
|
if (!normalizeCountryIso(countryIso).equals(countryIso)) {
|
|
throw new XmlPullParserException("Country code: " + countryIso
|
|
+ " is not normalized at " + debugInfo);
|
|
}
|
|
if (knownCountryCodes.contains(countryIso)) {
|
|
throw new XmlPullParserException("Second entry for country code: " + countryIso
|
|
+ " at " + debugInfo);
|
|
}
|
|
if (timeZoneMappings.isEmpty()) {
|
|
throw new XmlPullParserException("No time zone IDs for country code: " + countryIso
|
|
+ " at " + debugInfo);
|
|
}
|
|
if (!TimeZoneMapping.containsTimeZoneId(timeZoneMappings, defaultTimeZoneId)) {
|
|
throw new XmlPullParserException("defaultTimeZoneId for country code: "
|
|
+ countryIso + " is not one of the zones " + timeZoneMappings + " at "
|
|
+ debugInfo);
|
|
}
|
|
knownCountryCodes.add(countryIso);
|
|
|
|
return CONTINUE;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads just the IANA version from the file header. The version is then available via
|
|
* {@link #getIanaVersion()}.
|
|
*/
|
|
private static class IanaVersionExtractor implements TimeZonesProcessor {
|
|
|
|
private String ianaVersion;
|
|
|
|
@Override
|
|
public boolean processHeader(String ianaVersion) throws XmlPullParserException {
|
|
this.ianaVersion = ianaVersion;
|
|
return HALT;
|
|
}
|
|
|
|
public String getIanaVersion() {
|
|
return ianaVersion;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads all country time zone information into memory and makes it available as a
|
|
* {@link CountryZonesFinder}.
|
|
*/
|
|
private static class CountryZonesLookupExtractor implements TimeZonesProcessor {
|
|
private List<CountryTimeZones> countryTimeZonesList = new ArrayList<>(250 /* default */);
|
|
|
|
@Override
|
|
public boolean processCountryZones(String countryIso, String defaultTimeZoneId,
|
|
boolean defaultTimeZoneBoost, boolean everUsesUtc,
|
|
List<TimeZoneMapping> timeZoneMappings, String debugInfo)
|
|
throws XmlPullParserException {
|
|
|
|
CountryTimeZones countryTimeZones = CountryTimeZones
|
|
.createValidated(
|
|
countryIso, defaultTimeZoneId, defaultTimeZoneBoost, everUsesUtc,
|
|
timeZoneMappings, debugInfo);
|
|
countryTimeZonesList.add(countryTimeZones);
|
|
return CONTINUE;
|
|
}
|
|
|
|
CountryZonesFinder getCountryZonesLookup() {
|
|
return new CountryZonesFinder(countryTimeZonesList);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extracts <em>validated</em> time zones information associated with a specific country code.
|
|
* Processing is halted when the country code is matched and the validated result is also made
|
|
* available via {@link #getValidatedCountryTimeZones()}.
|
|
*/
|
|
private static class SelectiveCountryTimeZonesExtractor implements TimeZonesProcessor {
|
|
|
|
private final String countryCodeToMatch;
|
|
private CountryTimeZones validatedCountryTimeZones;
|
|
|
|
private SelectiveCountryTimeZonesExtractor(String countryCodeToMatch) {
|
|
this.countryCodeToMatch = normalizeCountryIso(countryCodeToMatch);
|
|
}
|
|
|
|
@Override
|
|
public boolean processCountryZones(String countryIso, String defaultTimeZoneId,
|
|
boolean defaultTimeZoneBoost, boolean everUsesUtc,
|
|
List<TimeZoneMapping> timeZoneMappings, String debugInfo) {
|
|
countryIso = normalizeCountryIso(countryIso);
|
|
if (!countryCodeToMatch.equals(countryIso)) {
|
|
return CONTINUE;
|
|
}
|
|
validatedCountryTimeZones = CountryTimeZones.createValidated(countryIso,
|
|
defaultTimeZoneId, defaultTimeZoneBoost, everUsesUtc, timeZoneMappings,
|
|
debugInfo);
|
|
|
|
return HALT;
|
|
}
|
|
|
|
/**
|
|
* Returns the CountryTimeZones that matched, or {@code null} if there were no matches.
|
|
*/
|
|
CountryTimeZones getValidatedCountryTimeZones() {
|
|
return validatedCountryTimeZones;
|
|
}
|
|
}
|
|
}
|