| /* |
| * Copyright (C) 2016 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 java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.nio.charset.StandardCharsets; |
| import java.util.Locale; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Version information associated with the set of time zone data on a device. |
| * |
| * <p>Time Zone Data Sets have a major ({@link #getFormatMajorVersion()}) and minor |
| * ({@link #currentFormatMinorVersion()}) version number: |
| * <ul> |
| * <li>Major version numbers are mutually incompatible. e.g. v2 is not compatible with a v1 or a |
| * v3 device.</li> |
| * <li>Minor version numbers are backwards compatible. e.g. a v2.2 data set will work |
| * on a v2.1 device but not a v2.3 device. The minor version is reset to 1 when the major version |
| * is incremented.</li> |
| * </ul> |
| * |
| * <p>Data sets contain time zone rules and other data associated wtih a tzdb release |
| * ({@link #getRulesVersion()}) and an additional Android-specific revision number |
| * ({@link #getRevision()}). |
| * |
| * <p>See platform/system/timezone/README.android for more information. |
| * @hide |
| */ |
| @libcore.api.CorePlatformApi |
| public final class TzDataSetVersion { |
| |
| // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797 |
| /** |
| * The name typically given to the {@link TzDataSetVersion} file. See |
| * {@link TzDataSetVersion#readFromFile(File)}. |
| */ |
| @libcore.api.CorePlatformApi |
| public static final String DEFAULT_FILE_NAME = "tz_version"; |
| |
| /** |
| * The major tz data format version supported by this device. |
| * Increment this for non-backwards compatible changes to the tz data format. Reset the minor |
| * version to 1 when doing so. |
| */ |
| // @VisibleForTesting : Keep this inline-able: it is used from CTS tests. |
| public static final int CURRENT_FORMAT_MAJOR_VERSION = 8; // Android V |
| |
| /** |
| * Returns the major tz data format version supported by this device. |
| */ |
| @libcore.api.CorePlatformApi |
| public static int currentFormatMajorVersion() { |
| return CURRENT_FORMAT_MAJOR_VERSION; |
| } |
| |
| /** |
| * The minor tz data format version supported by this device. Increment this for |
| * backwards-compatible changes to the tz data format. |
| */ |
| // @VisibleForTesting : Keep this inline-able: it is used from CTS tests. |
| public static final int CURRENT_FORMAT_MINOR_VERSION = 1; |
| |
| /** |
| * Returns the minor tz data format version supported by this device. |
| */ |
| @libcore.api.CorePlatformApi |
| public static int currentFormatMinorVersion() { |
| return CURRENT_FORMAT_MINOR_VERSION; |
| } |
| |
| /** The full major + minor tz data format version for this device. */ |
| private static final String FULL_CURRENT_FORMAT_VERSION_STRING = |
| toFormatVersionString(CURRENT_FORMAT_MAJOR_VERSION, CURRENT_FORMAT_MINOR_VERSION); |
| |
| private static final int FORMAT_VERSION_STRING_LENGTH = |
| FULL_CURRENT_FORMAT_VERSION_STRING.length(); |
| private static final Pattern FORMAT_VERSION_PATTERN = Pattern.compile("(\\d{3})\\.(\\d{3})"); |
| |
| /** A pattern that matches the IANA rules value of a rules update. e.g. "2016g" */ |
| private static final Pattern RULES_VERSION_PATTERN = Pattern.compile("(\\d{4}\\w)"); |
| |
| private static final int RULES_VERSION_LENGTH = 5; |
| |
| /** A pattern that matches the revision of a rules update. e.g. "001" */ |
| private static final Pattern REVISION_PATTERN = Pattern.compile("(\\d{3})"); |
| |
| private static final int REVISION_LENGTH = 3; |
| |
| /** |
| * The length of a well-formed tz data set version file: |
| * {Format version}|{Rule version}|{Revision} |
| */ |
| private static final int TZ_DATA_VERSION_FILE_LENGTH = FORMAT_VERSION_STRING_LENGTH + 1 |
| + RULES_VERSION_LENGTH |
| + 1 + REVISION_LENGTH; |
| |
| private static final Pattern TZ_DATA_VERSION_FILE_PATTERN = Pattern.compile( |
| FORMAT_VERSION_PATTERN.pattern() + "\\|" |
| + RULES_VERSION_PATTERN.pattern() + "\\|" |
| + REVISION_PATTERN.pattern() |
| + ".*" /* ignore trailing */); |
| |
| private final int formatMajorVersion; |
| private final int formatMinorVersion; |
| private final String rulesVersion; |
| private final int revision; |
| |
| @libcore.api.CorePlatformApi |
| public TzDataSetVersion(int formatMajorVersion, int formatMinorVersion, String rulesVersion, |
| int revision) throws TzDataSetException { |
| this.formatMajorVersion = validate3DigitVersion(formatMajorVersion); |
| this.formatMinorVersion = validate3DigitVersion(formatMinorVersion); |
| if (!RULES_VERSION_PATTERN.matcher(rulesVersion).matches()) { |
| throw new TzDataSetException("Invalid rulesVersion: " + rulesVersion); |
| } |
| this.rulesVersion = rulesVersion; |
| this.revision = validate3DigitVersion(revision); |
| } |
| |
| // VisibleForTesting |
| public static TzDataSetVersion fromBytes(byte[] bytes) throws TzDataSetException { |
| String tzDataVersion = new String(bytes, StandardCharsets.US_ASCII); |
| try { |
| Matcher matcher = TZ_DATA_VERSION_FILE_PATTERN.matcher(tzDataVersion); |
| if (!matcher.matches()) { |
| throw new TzDataSetException( |
| "Invalid tz data version string: \"" + tzDataVersion + "\""); |
| } |
| String formatMajorVersion = matcher.group(1); |
| String formatMinorVersion = matcher.group(2); |
| String rulesVersion = matcher.group(3); |
| String revision = matcher.group(4); |
| return new TzDataSetVersion( |
| from3DigitVersionString(formatMajorVersion), |
| from3DigitVersionString(formatMinorVersion), |
| rulesVersion, |
| from3DigitVersionString(revision)); |
| } catch (IndexOutOfBoundsException e) { |
| // The use of the regexp above should make this impossible. |
| throw new TzDataSetException( |
| "tz data version string too short: \"" + tzDataVersion + "\""); |
| } |
| } |
| |
| // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797 |
| @libcore.api.CorePlatformApi |
| public static TzDataSetVersion readFromFile(File file) throws IOException, TzDataSetException { |
| byte[] versionBytes = readBytes(file, TzDataSetVersion.TZ_DATA_VERSION_FILE_LENGTH); |
| return fromBytes(versionBytes); |
| } |
| |
| /** Returns the major version number. See {@link TzDataSetVersion}. */ |
| @libcore.api.CorePlatformApi |
| public int getFormatMajorVersion() { |
| return formatMajorVersion; |
| } |
| |
| /** Returns the minor version number. See {@link TzDataSetVersion}. */ |
| @libcore.api.CorePlatformApi |
| public int getFormatMinorVersion() { |
| return formatMinorVersion; |
| } |
| |
| /** Returns the tzdb version string. See {@link TzDataSetVersion}. */ |
| @libcore.api.CorePlatformApi |
| public String getRulesVersion() { |
| return rulesVersion; |
| } |
| |
| // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797 |
| /** Returns the Android revision. See {@link TzDataSetVersion}. */ |
| @libcore.api.CorePlatformApi |
| public int getRevision() { |
| return revision; |
| } |
| |
| // Remove from CorePlatformApi when all users in platform code are removed. http://b/123398797 |
| @libcore.api.CorePlatformApi |
| public byte[] toBytes() { |
| return toBytes(formatMajorVersion, formatMinorVersion, rulesVersion, revision); |
| } |
| |
| private static byte[] toBytes( |
| int majorFormatVersion, int minorFormatVerison, String rulesVersion, int revision) { |
| return (toFormatVersionString(majorFormatVersion, minorFormatVerison) |
| + "|" + rulesVersion + "|" + to3DigitVersionString(revision)) |
| .getBytes(StandardCharsets.US_ASCII); |
| } |
| |
| @libcore.api.CorePlatformApi |
| public static boolean isCompatibleWithThisDevice(TzDataSetVersion tzDataVersion) { |
| return (CURRENT_FORMAT_MAJOR_VERSION == tzDataVersion.formatMajorVersion) |
| && (CURRENT_FORMAT_MINOR_VERSION <= tzDataVersion.formatMinorVersion); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (o == null || getClass() != o.getClass()) { |
| return false; |
| } |
| TzDataSetVersion that = (TzDataSetVersion) o; |
| if (formatMajorVersion != that.formatMajorVersion) { |
| return false; |
| } |
| if (formatMinorVersion != that.formatMinorVersion) { |
| return false; |
| } |
| if (revision != that.revision) { |
| return false; |
| } |
| return rulesVersion.equals(that.rulesVersion); |
| } |
| |
| @Override |
| public int hashCode() { |
| int result = formatMajorVersion; |
| result = 31 * result + formatMinorVersion; |
| result = 31 * result + rulesVersion.hashCode(); |
| result = 31 * result + revision; |
| return result; |
| } |
| |
| @Override |
| public String toString() { |
| return "TzDataSetVersion{" + |
| "formatMajorVersion=" + formatMajorVersion + |
| ", formatMinorVersion=" + formatMinorVersion + |
| ", rulesVersion='" + rulesVersion + '\'' + |
| ", revision=" + revision + |
| '}'; |
| } |
| |
| /** |
| * Returns a version as a zero-padded three-digit String value. |
| */ |
| private static String to3DigitVersionString(int version) { |
| try { |
| return String.format(Locale.ROOT, "%03d", validate3DigitVersion(version)); |
| } catch (TzDataSetException e) { |
| throw new IllegalArgumentException(e); |
| } |
| } |
| |
| /** |
| * Validates and parses a zero-padded three-digit String value. |
| */ |
| private static int from3DigitVersionString(String versionString) throws TzDataSetException { |
| final String parseErrorMessage = "versionString must be a zero padded, 3 digit, positive" |
| + " decimal integer"; |
| if (versionString.length() != 3) { |
| throw new TzDataSetException(parseErrorMessage); |
| } |
| try { |
| int version = Integer.parseInt(versionString); |
| return validate3DigitVersion(version); |
| } catch (NumberFormatException e) { |
| throw new TzDataSetException(parseErrorMessage, e); |
| } |
| } |
| |
| private static int validate3DigitVersion(int value) throws TzDataSetException { |
| // 0 is allowed but is reserved for testing. |
| if (value < 0 || value > 999) { |
| throw new TzDataSetException("Expected 0 <= value <= 999, was " + value); |
| } |
| return value; |
| } |
| |
| private static String toFormatVersionString(int majorFormatVersion, int minorFormatVersion) { |
| return to3DigitVersionString(majorFormatVersion) |
| + "." + to3DigitVersionString(minorFormatVersion); |
| } |
| |
| /** |
| * Reads up to {@code maxBytes} bytes from the specified file. The returned array can be |
| * shorter than {@code maxBytes} if the file is shorter. |
| */ |
| private static byte[] readBytes(File file, int maxBytes) throws IOException { |
| if (maxBytes <= 0) { |
| throw new IllegalArgumentException("maxBytes ==" + maxBytes); |
| } |
| |
| try (FileInputStream in = new FileInputStream(file)) { |
| byte[] max = new byte[maxBytes]; |
| int bytesRead = in.read(max, 0, maxBytes); |
| byte[] toReturn = new byte[bytesRead]; |
| System.arraycopy(max, 0, toReturn, 0, bytesRead); |
| return toReturn; |
| } |
| } |
| |
| /** |
| * A checked exception used in connection with time zone data sets. |
| */ |
| @libcore.api.CorePlatformApi |
| public static class TzDataSetException extends Exception { |
| |
| @libcore.api.CorePlatformApi |
| public TzDataSetException(String message) { |
| super(message); |
| } |
| |
| @libcore.api.CorePlatformApi |
| public TzDataSetException(String message, Throwable cause) { |
| super(message, cause); |
| } |
| } |
| } |