diff options
13 files changed, 1050 insertions, 156 deletions
diff --git a/benchmarks/src/benchmarks/regression/MathBenchmark.java b/benchmarks/src/benchmarks/regression/MathBenchmark.java index 2227c7f..19b2162 100644 --- a/benchmarks/src/benchmarks/regression/MathBenchmark.java +++ b/benchmarks/src/benchmarks/regression/MathBenchmark.java @@ -302,7 +302,7 @@ public class MathBenchmark extends SimpleBenchmark { public long timeMinL(int reps) { long result = l; for (int rep = 0; rep < reps; ++rep) { - Math.min(l, l); + result = Math.min(l, l); } return result; } diff --git a/harmony-tests/src/test/java/org/apache/harmony/tests/java/text/CollationElementIteratorTest.java b/harmony-tests/src/test/java/org/apache/harmony/tests/java/text/CollationElementIteratorTest.java index 0ca489c..081b446 100644 --- a/harmony-tests/src/test/java/org/apache/harmony/tests/java/text/CollationElementIteratorTest.java +++ b/harmony-tests/src/test/java/org/apache/harmony/tests/java/text/CollationElementIteratorTest.java @@ -125,7 +125,8 @@ public class CollationElementIteratorTest extends TestCase { public void testGetMaxExpansion() { String text = "cha"; - RuleBasedCollator rbColl = (RuleBasedCollator) Collator.getInstance(new Locale("es", "", "TRADITIONAL")); + RuleBasedCollator rbColl = (RuleBasedCollator) Collator.getInstance( + Locale.forLanguageTag("es-u-co-trad")); CollationElementIterator iterator = rbColl.getCollationElementIterator(text); int order = iterator.next(); while (order != CollationElementIterator.NULLORDER) { @@ -177,7 +178,8 @@ public class CollationElementIteratorTest extends TestCase { } public void testSetOffset() { - RuleBasedCollator rbColl = (RuleBasedCollator) Collator.getInstance(new Locale("es", "", "TRADITIONAL")); + RuleBasedCollator rbColl = (RuleBasedCollator) Collator.getInstance( + Locale.forLanguageTag("es-u-co-trad")); String text = "cha"; CollationElementIterator iterator = rbColl.getCollationElementIterator(text); iterator.setOffset(0); @@ -189,7 +191,8 @@ public class CollationElementIteratorTest extends TestCase { } public void testSetTextString() { - RuleBasedCollator rbColl = (RuleBasedCollator) Collator.getInstance(new Locale("es", "", "TRADITIONAL")); + RuleBasedCollator rbColl = (RuleBasedCollator) Collator.getInstance( + Locale.forLanguageTag("es-u-co-trad")); String text = "caa"; CollationElementIterator iterator = rbColl.getCollationElementIterator(text); iterator.setOffset(0); @@ -208,7 +211,8 @@ public class CollationElementIteratorTest extends TestCase { } public void testSetTextCharacterIterator() { - RuleBasedCollator rbColl = (RuleBasedCollator) Collator.getInstance(new Locale("es", "", "TRADITIONAL")); + RuleBasedCollator rbColl = (RuleBasedCollator) Collator.getInstance( + Locale.forLanguageTag("es-u-co-trad")); String text = "caa"; CollationElementIterator iterator = rbColl.getCollationElementIterator(text); iterator.setOffset(1); diff --git a/harmony-tests/src/test/java/org/apache/harmony/tests/java/text/RuleBasedCollatorTest.java b/harmony-tests/src/test/java/org/apache/harmony/tests/java/text/RuleBasedCollatorTest.java index f5a8057..906857b 100644 --- a/harmony-tests/src/test/java/org/apache/harmony/tests/java/text/RuleBasedCollatorTest.java +++ b/harmony-tests/src/test/java/org/apache/harmony/tests/java/text/RuleBasedCollatorTest.java @@ -105,7 +105,7 @@ public class RuleBasedCollatorTest extends TestCase { public void testGetCollationElementIteratorString() throws Exception { { - Locale locale = new Locale("es", "", "TRADITIONAL"); + Locale locale = Locale.forLanguageTag("es-u-co-trad"); RuleBasedCollator coll = (RuleBasedCollator) Collator.getInstance(locale); String source = "cha"; CollationElementIterator iterator = coll.getCollationElementIterator(source); @@ -147,7 +147,7 @@ public class RuleBasedCollatorTest extends TestCase { public void testGetCollationElementIteratorCharacterIterator() throws Exception { { - Locale locale = new Locale("es", "", "TRADITIONAL"); + Locale locale = Locale.forLanguageTag("es-u-co-trad"); RuleBasedCollator coll = (RuleBasedCollator) Collator.getInstance(locale); String text = "cha"; StringCharacterIterator source = new StringCharacterIterator(text); diff --git a/luni/src/main/java/java/nio/ByteBuffer.java b/luni/src/main/java/java/nio/ByteBuffer.java index 5873590..61093fa 100644 --- a/luni/src/main/java/java/nio/ByteBuffer.java +++ b/luni/src/main/java/java/nio/ByteBuffer.java @@ -69,7 +69,11 @@ public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer if (capacity < 0) { throw new IllegalArgumentException("capacity < 0: " + capacity); } - return new DirectByteBuffer(MemoryBlock.allocate(capacity), capacity, 0, false, null); + // Ensure alignment by 8. + MemoryBlock memoryBlock = MemoryBlock.allocate(capacity + 7); + long address = memoryBlock.toLong(); + long alignedAddress = (address + 7) & ~(long)7; + return new DirectByteBuffer(memoryBlock, capacity, (int)(alignedAddress - address), false, null); } /** diff --git a/luni/src/main/java/java/util/GregorianCalendar.java b/luni/src/main/java/java/util/GregorianCalendar.java index 71b79dd..be96684 100644 --- a/luni/src/main/java/java/util/GregorianCalendar.java +++ b/luni/src/main/java/java/util/GregorianCalendar.java @@ -331,7 +331,16 @@ public class GregorianCalendar extends Calendar { setTimeInMillis(System.currentTimeMillis()); } - GregorianCalendar(boolean ignored) { + /** + * A minimum-cost constructor that does not initialize the current time or perform any date + * calculations. For use internally when the time will be set later. Other constructors, such as + * {@link GregorianCalendar#GregorianCalendar()}, set the time to the current system clock + * and recalculate the fields incurring unnecessary cost when the time or fields will be set + * later. + * + * @hide used internally + */ + public GregorianCalendar(boolean ignored) { super(TimeZone.getDefault()); setFirstDayOfWeek(SUNDAY); setMinimalDaysInFirstWeek(1); diff --git a/luni/src/main/java/java/util/Locale.java b/luni/src/main/java/java/util/Locale.java index a3eaf21..e0582dc 100644 --- a/luni/src/main/java/java/util/Locale.java +++ b/luni/src/main/java/java/util/Locale.java @@ -270,6 +270,11 @@ public final class Locale implements Cloneable, Serializable { public static final char UNICODE_LOCALE_EXTENSION = 'u'; /** + * ISO 639-3 generic code for undetermined languages. + */ + private static final String UNDETERMINED_LANGUAGE = "und"; + + /** * The current default locale. It is temporarily assigned to US because we * need a default locale to lookup the real default locale. */ @@ -340,18 +345,22 @@ public final class Locale implements Cloneable, Serializable { * @throws IllformedLocaleException if the language was invalid. */ public Builder setLanguage(String language) { - this.language = normalizeAndValidateLanguage(language); + this.language = normalizeAndValidateLanguage(language, true /* strict */); return this; } - private static String normalizeAndValidateLanguage(String language) { + private static String normalizeAndValidateLanguage(String language, boolean strict) { if (language == null || language.isEmpty()) { return ""; } final String lowercaseLanguage = language.toLowerCase(Locale.ROOT); if (!isValidBcp47Alpha(lowercaseLanguage, 2, 3)) { - throw new IllformedLocaleException("Invalid language: " + language); + if (strict) { + throw new IllformedLocaleException("Invalid language: " + language); + } else { + return UNDETERMINED_LANGUAGE; + } } return lowercaseLanguage; @@ -377,7 +386,7 @@ public final class Locale implements Cloneable, Serializable { return this; } - final Locale fromIcu = ICU.forLanguageTag(languageTag, true /* strict */); + final Locale fromIcu = forLanguageTag(languageTag, true /* strict */); // When we ask ICU for strict parsing, it might return a null locale // if the language tag is malformed. if (fromIcu == null) { @@ -400,11 +409,11 @@ public final class Locale implements Cloneable, Serializable { * @throws IllformedLocaleException if {@code} region is invalid. */ public Builder setRegion(String region) { - this.region = normalizeAndValidateRegion(region); + this.region = normalizeAndValidateRegion(region, true /* strict */); return this; } - private static String normalizeAndValidateRegion(String region) { + private static String normalizeAndValidateRegion(String region, boolean strict) { if (region == null || region.isEmpty()) { return ""; } @@ -412,7 +421,11 @@ public final class Locale implements Cloneable, Serializable { final String uppercaseRegion = region.toUpperCase(Locale.ROOT); if (!isValidBcp47Alpha(uppercaseRegion, 2, 2) && !isUnM49AreaCode(uppercaseRegion)) { - throw new IllformedLocaleException("Invalid region: " + region); + if (strict) { + throw new IllformedLocaleException("Invalid region: " + region); + } else { + return ""; + } } return uppercaseRegion; @@ -453,27 +466,32 @@ public final class Locale implements Cloneable, Serializable { String[] subTags = normalizedVariant.split("_"); for (String subTag : subTags) { - // The BCP-47 spec states that : - // - Subtags can be between [5, 8] alphanumeric chars in length. - // - Subtags that start with a number are allowed to be 4 chars in length. - if (subTag.length() >= 5 && subTag.length() <= 8) { - if (!isAsciiAlphaNum(subTag)) { - throw new IllformedLocaleException("Invalid variant: " + variant); - } - } else if (subTag.length() == 4) { - final char firstChar = subTag.charAt(0); - if (!(firstChar >= '0' && firstChar <= '9') || !isAsciiAlphaNum(subTag)) { - throw new IllformedLocaleException("Invalid variant: " + variant); - } - } else { + if (!isValidVariantSubtag(subTag)) { throw new IllformedLocaleException("Invalid variant: " + variant); } } - return normalizedVariant; } + private static boolean isValidVariantSubtag(String subTag) { + // The BCP-47 spec states that : + // - Subtags can be between [5, 8] alphanumeric chars in length. + // - Subtags that start with a number are allowed to be 4 chars in length. + if (subTag.length() >= 5 && subTag.length() <= 8) { + if (isAsciiAlphaNum(subTag)) { + return true; + } + } else if (subTag.length() == 4) { + final char firstChar = subTag.charAt(0); + if ((firstChar >= '0' && firstChar <= '9') && isAsciiAlphaNum(subTag)) { + return true; + } + } + + return false; + } + /** * Sets the locale script. If {@code script} is {@code null} or empty, * the previous value is cleared. @@ -489,17 +507,24 @@ public final class Locale implements Cloneable, Serializable { * @throws IllformedLocaleException if {@code script} is invalid. */ public Builder setScript(String script) { + this.script = normalizeAndValidateScript(script, true /* strict */); + return this; + } + + private static String normalizeAndValidateScript(String script, boolean strict) { if (script == null || script.isEmpty()) { - this.script = ""; - return this; + return ""; } if (!isValidBcp47Alpha(script, 4, 4)) { - throw new IllformedLocaleException("Invalid script: " + script); + if (strict) { + throw new IllformedLocaleException("Invalid script: " + script); + } else { + return ""; + } } - this.script = titleCaseAsciiWord(script); - return this; + return titleCaseAsciiWord(script); } /** @@ -795,7 +820,7 @@ public final class Locale implements Cloneable, Serializable { throw new NullPointerException("languageTag == null"); } - return ICU.forLanguageTag(languageTag, false /* strict */); + return forLanguageTag(languageTag, false /* strict */); } private transient String countryCode; @@ -1005,9 +1030,9 @@ public final class Locale implements Cloneable, Serializable { return ""; } - try { - Builder.normalizeAndValidateRegion(countryCode); - } catch (IllformedLocaleException ex) { + final String normalizedRegion = Builder.normalizeAndValidateRegion( + countryCode, false /* strict */); + if (normalizedRegion.isEmpty()) { return countryCode; } @@ -1041,9 +1066,9 @@ public final class Locale implements Cloneable, Serializable { // display language for the indeterminate language code. // // Sigh... ugh... and what not. - try { - Builder.normalizeAndValidateLanguage(languageCode); - } catch (IllformedLocaleException ex) { + final String normalizedLanguage = Builder.normalizeAndValidateLanguage( + languageCode, false /* strict */); + if (UNDETERMINED_LANGUAGE.equals(normalizedLanguage)) { return languageCode; } @@ -1331,17 +1356,8 @@ public final class Locale implements Cloneable, Serializable { // in the builder, but we must use hyphens in the BCP-47 language tag. variant = variantCode.replace('_', '-'); } else { - try { - language = Builder.normalizeAndValidateLanguage(languageCode); - } catch (IllformedLocaleException ilfe) { - // Ignored, continue processing with "". - } - - try { - region = Builder.normalizeAndValidateRegion(countryCode); - } catch (IllformedLocaleException ilfe) { - // Ignored, continue processing with "". - } + language = Builder.normalizeAndValidateLanguage(languageCode, false /* strict */); + region = Builder.normalizeAndValidateRegion(countryCode, false /* strict */); try { variant = Builder.normalizeAndValidateVariant(variantCode); @@ -1358,7 +1374,7 @@ public final class Locale implements Cloneable, Serializable { } if (language.isEmpty()) { - language = "und"; + language = UNDETERMINED_LANGUAGE; } if ("no".equals(language) && "NO".equals(region) && "NY".equals(variant)) { @@ -1497,7 +1513,7 @@ public final class Locale implements Cloneable, Serializable { private static String concatenateRange(String[] array, int start, int end) { StringBuilder builder = new StringBuilder(32); for (int i = start; i < end; ++i) { - if (i != 0) { + if (i != start) { builder.append('-'); } builder.append(array[i]); @@ -1922,8 +1938,10 @@ public final class Locale implements Cloneable, Serializable { while (true) { final Map.Entry<String, String> keyWord = keywordsIterator.next(); sb.append(keyWord.getKey()); - sb.append('-'); - sb.append(keyWord.getValue()); + if (!keyWord.getValue().isEmpty()) { + sb.append('-'); + sb.append(keyWord.getValue()); + } if (keywordsIterator.hasNext()) { sb.append('-'); } else { @@ -1970,6 +1988,8 @@ public final class Locale implements Cloneable, Serializable { if (subtagsForKeyword.size() > 0) { keywords.put(lastKeyword, joinBcp47Subtags(subtagsForKeyword)); + } else { + keywords.put(lastKeyword, ""); } } @@ -1991,7 +2011,10 @@ public final class Locale implements Cloneable, Serializable { return sb.toString(); } - private static String adjustLanguageCode(String languageCode) { + /** + * @hide for internal use only. + */ + public static String adjustLanguageCode(String languageCode) { String adjusted = languageCode.toLowerCase(Locale.US); // Map new language codes to the obsolete language // codes so the correct resource bundles will be used. @@ -2005,4 +2028,230 @@ public final class Locale implements Cloneable, Serializable { return adjusted; } + + /** + * Map of grandfathered language tags to their modern replacements. + */ + private static final TreeMap<String, String> GRANDFATHERED_LOCALES; + + static { + GRANDFATHERED_LOCALES = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER); + + // From http://tools.ietf.org/html/bcp47 + // + // grandfathered = irregular ; non-redundant tags registered + // / regular ; during the RFC 3066 era + // irregular = + GRANDFATHERED_LOCALES.put("en-GB-oed", "en-GB-x-oed"); + GRANDFATHERED_LOCALES.put("i-ami", "ami"); + GRANDFATHERED_LOCALES.put("i-bnn", "bnn"); + GRANDFATHERED_LOCALES.put("i-default", "en-x-i-default"); + GRANDFATHERED_LOCALES.put("i-enochian", "und-x-i-enochian"); + GRANDFATHERED_LOCALES.put("i-hak", "hak"); + GRANDFATHERED_LOCALES.put("i-klingon", "tlh"); + GRANDFATHERED_LOCALES.put("i-lux", "lb"); + GRANDFATHERED_LOCALES.put("i-mingo", "see-x-i-mingo"); + GRANDFATHERED_LOCALES.put("i-navajo", "nv"); + GRANDFATHERED_LOCALES.put("i-pwn", "pwn"); + GRANDFATHERED_LOCALES.put("i-tao", "tao"); + GRANDFATHERED_LOCALES.put("i-tay", "tay"); + GRANDFATHERED_LOCALES.put("i-tsu", "tsu"); + GRANDFATHERED_LOCALES.put("sgn-BE-FR", "sfb"); + GRANDFATHERED_LOCALES.put("sgn-BE-NL", "vgt"); + GRANDFATHERED_LOCALES.put("sgn-CH-DE", "sgg"); + + // regular = + GRANDFATHERED_LOCALES.put("art-lojban", "jbo"); + GRANDFATHERED_LOCALES.put("cel-gaulish", "xtg-x-cel-gaulish"); + GRANDFATHERED_LOCALES.put("no-bok", "nb"); + GRANDFATHERED_LOCALES.put("no-nyn", "nn"); + GRANDFATHERED_LOCALES.put("zh-guoyu", "cmn"); + GRANDFATHERED_LOCALES.put("zh-hakka", "hak"); + GRANDFATHERED_LOCALES.put("zh-min", "nan-x-zh-min"); + GRANDFATHERED_LOCALES.put("zh-min-nan", "nan"); + GRANDFATHERED_LOCALES.put("zh-xiang", "hsn"); + } + + private static String convertGrandfatheredTag(String original) { + final String converted = GRANDFATHERED_LOCALES.get(original); + return converted != null ? converted : original; + } + + /** + * Scans elements of {@code subtags} in the range {@code [startIndex, endIndex)} + * and appends valid variant subtags upto the first invalid subtag (if any) to + * {@code normalizedVariants}. All appended variant subtags are converted to uppercase. + */ + private static void extractVariantSubtags(String[] subtags, int startIndex, int endIndex, + List<String> normalizedVariants) { + for (int i = startIndex; i < endIndex; i++) { + final String subtag = subtags[i]; + + if (Builder.isValidVariantSubtag(subtag)) { + normalizedVariants.add(subtag.toUpperCase(Locale.ROOT)); + } else { + break; + } + } + } + + /** + * Scans elements of {@code subtags} in the range {@code [startIndex, endIndex)} + * and inserts valid extensions into {@code extensions}. The scan is aborted + * when an invalid extension is encountered. Returns the index of the first + * unparsable element of {@code subtags}. + */ + private static int extractExtensions(String[] subtags, int startIndex, int endIndex, + Map<Character, String> extensions) { + int privateUseExtensionIndex = -1; + int extensionKeyIndex = -1; + + int i = startIndex; + for (; i < endIndex; i++) { + final String subtag = subtags[i]; + + final boolean parsingPrivateUse = (privateUseExtensionIndex != -1) && + (extensionKeyIndex == privateUseExtensionIndex); + + // Note that private use extensions allow subtags of length 1. + // Private use extensions *must* come last, so there's no ambiguity + // in that case. + if (subtag.length() == 1 && !parsingPrivateUse) { + // Emit the last extension we encountered if any. First check + // whether we encountered two keys in a row (which is an error). + // Also checks if we already have an extension with the same key, + // which is again an error. + if (extensionKeyIndex != -1) { + if ((i - 1) == extensionKeyIndex) { + return extensionKeyIndex; + } + + final String key = subtags[extensionKeyIndex]; + if (extensions.containsKey(key.charAt(0))) { + return extensionKeyIndex; + } + + final String value = concatenateRange(subtags, extensionKeyIndex + 1, i); + extensions.put(key.charAt(0), value.toLowerCase(Locale.ROOT)); + } + + // Mark the start of the next extension. Also keep track of whether this + // is a private use extension, and throw an error if it doesn't come last. + extensionKeyIndex = i; + if ("x".equals(subtag)) { + privateUseExtensionIndex = i; + } else if (privateUseExtensionIndex != -1) { + // The private use extension must come last. + return privateUseExtensionIndex; + } + } else if (extensionKeyIndex != -1) { + // We must have encountered a valid key in order to start parsing + // its subtags. + if (!isValidBcp47Alphanum(subtag, parsingPrivateUse ? 1 : 2, 8)) { + return i; + } + } else { + // Encountered a value without a preceding key. + return i; + } + } + + if (extensionKeyIndex != -1) { + if ((i - 1) == extensionKeyIndex) { + return extensionKeyIndex; + } + + final String key = subtags[extensionKeyIndex]; + if (extensions.containsKey(key.charAt(0))) { + return extensionKeyIndex; + } + + final String value = concatenateRange(subtags, extensionKeyIndex + 1, i); + extensions.put(key.charAt(0), value.toLowerCase(Locale.ROOT)); + } + + return i; + } + + private static Locale forLanguageTag(/* @Nonnull */ String tag, boolean strict) { + final String converted = convertGrandfatheredTag(tag); + final String[] subtags = converted.split("-"); + + int lastSubtag = subtags.length; + for (int i = 0; i < subtags.length; ++i) { + final String subtag = subtags[i]; + if (subtag.isEmpty() || subtag.length() > 8) { + if (strict) { + throw new IllformedLocaleException("Invalid subtag at index: " + i + + " in tag: " + tag); + } else { + lastSubtag = (i - 1); + } + + break; + } + } + + final String languageCode = Builder.normalizeAndValidateLanguage(subtags[0], strict); + String scriptCode = ""; + int nextSubtag = 1; + if (lastSubtag > nextSubtag) { + scriptCode = Builder.normalizeAndValidateScript(subtags[nextSubtag], false /* strict */); + if (!scriptCode.isEmpty()) { + nextSubtag++; + } + } + + String regionCode = ""; + if (lastSubtag > nextSubtag) { + regionCode = Builder.normalizeAndValidateRegion(subtags[nextSubtag], false /* strict */); + if (!regionCode.isEmpty()) { + nextSubtag++; + } + } + + List<String> variants = null; + if (lastSubtag > nextSubtag) { + variants = new ArrayList<String>(); + extractVariantSubtags(subtags, nextSubtag, lastSubtag, variants); + nextSubtag += variants.size(); + } + + Map<Character, String> extensions = Collections.EMPTY_MAP; + if (lastSubtag > nextSubtag) { + extensions = new TreeMap<Character, String>(); + nextSubtag = extractExtensions(subtags, nextSubtag, lastSubtag, extensions); + } + + if (nextSubtag != lastSubtag) { + if (strict) { + throw new IllformedLocaleException("Unparseable subtag: " + subtags[nextSubtag] + + " from language tag: " + tag); + } + } + + Set<String> unicodeKeywords = Collections.EMPTY_SET; + Map<String, String> unicodeAttributes = Collections.EMPTY_MAP; + if (extensions.containsKey(UNICODE_LOCALE_EXTENSION)) { + unicodeKeywords = new TreeSet<String>(); + unicodeAttributes = new TreeMap<String, String>(); + parseUnicodeExtension(extensions.get(UNICODE_LOCALE_EXTENSION).split("-"), + unicodeAttributes, unicodeKeywords); + } + + String variantCode = ""; + if (variants != null && !variants.isEmpty()) { + StringBuilder variantsBuilder = new StringBuilder(variants.size() * 8); + for (int i = 0; i < variants.size(); ++i) { + if (i != 0) { + variantsBuilder.append('_'); + } + variantsBuilder.append(variants.get(i)); + } + variantCode = variantsBuilder.toString(); + } + + return new Locale(languageCode, regionCode, variantCode, scriptCode, + unicodeKeywords, unicodeAttributes, extensions, true /* has validated fields */); + } } diff --git a/luni/src/main/java/libcore/icu/ICU.java b/luni/src/main/java/libcore/icu/ICU.java index 33c899e..bb57f49 100644 --- a/luni/src/main/java/libcore/icu/ICU.java +++ b/luni/src/main/java/libcore/icu/ICU.java @@ -58,18 +58,6 @@ public final class ICU { return isoCountries.clone(); } - public static Locale forLanguageTag(String languageTag, boolean strict) { - final String icuLocaleId = localeForLanguageTag(languageTag, strict); - if (icuLocaleId == null) { - // TODO: We should probably return "und" here. From what I can tell, - // this happens only when the language in the languageTag is bad. - // Investigate this a bit more. - return null; - } - - return localeFromIcuLocaleId(icuLocaleId); - } - private static final int IDX_LANGUAGE = 0; private static final int IDX_SCRIPT = 1; private static final int IDX_REGION = 2; @@ -444,8 +432,6 @@ public final class ICU { private static native String[] getISOLanguagesNative(); private static native String[] getISOCountriesNative(); - private static native String localeForLanguageTag(String languageTag, boolean strict); - static native boolean initLocaleDataNative(String locale, LocaleData result); /** diff --git a/luni/src/main/java/libcore/util/ZoneInfo.java b/luni/src/main/java/libcore/util/ZoneInfo.java index ed7ab64..fbd120b 100644 --- a/luni/src/main/java/libcore/util/ZoneInfo.java +++ b/luni/src/main/java/libcore/util/ZoneInfo.java @@ -13,11 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +/* + * Elements of the WallTime class are a port of Bionic's localtime.c to Java. That code had the + * following header: + * + * This file is in the public domain, so clarified as of + * 1996-06-05 by Arthur David Olson. + */ package libcore.util; import java.util.Arrays; +import java.util.Calendar; import java.util.Date; +import java.util.GregorianCalendar; import java.util.TimeZone; import libcore.io.BufferIterator; @@ -310,4 +318,655 @@ public final class ZoneInfo extends TimeZone { // respectively. return super.clone(); } + + /** + * A class that represents a "wall time". This class is modeled on the C tm struct and + * is used to support android.text.format.Time behavior. Unlike the tm struct the year is + * represented as the full year, not the years since 1900. + * + * <p>This class contains a rewrite of various native functions that android.text.format.Time + * once relied on such as mktime_tz and localtime_tz. This replacement does not support leap + * seconds but does try to preserve behavior around ambiguous date/times found in the BSD + * version of mktime that was previously used. + * + * <p>The original native code used a 32-bit value for time_t on 32-bit Android, which + * was the only variant of Android available at the time. To preserve old behavior this code + * deliberately uses {@code int} rather than {@code long} for most things and performs + * calculations in seconds. This creates deliberate truncation issues for date / times before + * 1901 and after 2038. This is intentional but might be fixed in future if all the knock-ons + * can be resolved: Application code may have come to rely on the range so previously values + * like zero for year could indicate an invalid date but if we move to long the year zero would + * be valid. + * + * <p>All offsets are considered to be safe for addition / subtraction / multiplication without + * worrying about overflow. All absolute time arithmetic is checked for overflow / underflow. + */ + public static class WallTime { + + // We use a GregorianCalendar (set to UTC) to handle all the date/time normalization logic + // and to convert from a broken-down date/time to a millis value. + // Unfortunately, it cannot represent an initial state with a zero day and would + // automatically normalize it, so we must copy values into and out of it as needed. + private final GregorianCalendar calendar; + + private int year; + private int month; + private int monthDay; + private int hour; + private int minute; + private int second; + private int weekDay; + private int yearDay; + private int isDst; + private int gmtOffsetSeconds; + + public WallTime() { + this.calendar = new GregorianCalendar(false); + calendar.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + /** + * Sets the wall time to a point in time using the time zone information provided. This + * is a replacement for the old native localtime_tz() function. + * + * <p>When going from an instant to a wall time it is always unambiguous because there + * is only one offset rule acting at any given instant. We do not consider leap seconds. + */ + public void localtime(int timeSeconds, ZoneInfo zoneInfo) { + try { + int offsetSeconds = zoneInfo.mRawOffset / 1000; + + // Find out the timezone DST state and adjustment. + byte isDst; + if (zoneInfo.mTransitions.length == 0) { + isDst = 0; + } else { + // transitionIndex can be in the range -1..zoneInfo.mTransitions.length - 1 + int transitionIndex = findTransitionIndex(zoneInfo, timeSeconds); + if (transitionIndex < 0) { + // -1 means timeSeconds is "before the first recorded transition". The first + // recorded transition is treated as a transition from non-DST and the raw + // offset. + isDst = 0; + } else { + byte transitionType = zoneInfo.mTypes[transitionIndex]; + offsetSeconds += zoneInfo.mOffsets[transitionType]; + isDst = zoneInfo.mIsDsts[transitionType]; + } + } + + // Perform arithmetic that might underflow before setting fields. + int wallTimeSeconds = checkedAdd(timeSeconds, offsetSeconds); + + // Set fields. + calendar.setTimeInMillis(wallTimeSeconds * 1000L); + copyFieldsFromCalendar(); + this.isDst = isDst; + this.gmtOffsetSeconds = offsetSeconds; + } catch (CheckedArithmeticException e) { + // Just stop, leaving fields untouched. + } + } + + /** + * Returns the time in seconds since beginning of the Unix epoch for the wall time using the + * time zone information provided. This is a replacement for an old native mktime_tz() C + * function. + * + * <p>When going from a wall time to an instant the answer can be ambiguous. A wall + * time can map to zero, one or two instants given sane date/time transitions. Sane + * in this case means that transitions occur less frequently than the offset + * differences between them (which could cause all sorts of craziness like the + * skipping out of transitions). + * + * <p>For example, this is not fully supported: + * <ul> + * <li>t1 { time = 1, offset = 0 } + * <li>t2 { time = 2, offset = -1 } + * <li>t3 { time = 3, offset = -2 } + * </ul> + * A wall time in this case might map to t1, t2 or t3. + * + * <p>We do not handle leap seconds. + * <p>We assume that no timezone offset transition has an absolute offset > 24 hours. + * <p>We do not assume that adjacent transitions modify the DST state; adjustments can + * occur for other reasons such as when a zone changes its raw offset. + */ + public int mktime(ZoneInfo zoneInfo) { + // Normalize isDst to -1, 0 or 1 to simplify isDst equality checks below. + this.isDst = this.isDst > 0 ? this.isDst = 1 : this.isDst < 0 ? this.isDst = -1 : 0; + + copyFieldsToCalendar(); + final long longWallTimeSeconds = calendar.getTimeInMillis() / 1000; + if (Integer.MIN_VALUE > longWallTimeSeconds + || longWallTimeSeconds > Integer.MAX_VALUE) { + // For compatibility with the old native 32-bit implementation we must treat + // this as an error. Note: -1 could be confused with a real time. + return -1; + } + + try { + final int wallTimeSeconds = (int) longWallTimeSeconds; + final int rawOffsetSeconds = zoneInfo.mRawOffset / 1000; + final int rawTimeSeconds = checkedSubtract(wallTimeSeconds, rawOffsetSeconds); + + if (zoneInfo.mTransitions.length == 0) { + // There is no transition information. There is just a raw offset for all time. + if (this.isDst > 0) { + // Caller has asserted DST, but there is no DST information available. + return -1; + } + copyFieldsFromCalendar(); + this.isDst = 0; + this.gmtOffsetSeconds = rawOffsetSeconds; + return rawTimeSeconds; + } + + // We cannot know for sure what instant the wall time will map to. Unfortunately, in + // order to know for sure we need the timezone information, but to get the timezone + // information we need an instant. To resolve this we use the raw offset to find an + // OffsetInterval; this will get us the OffsetInterval we need or very close. + + // The initialTransition can be between -1 and (zoneInfo.mTransitions - 1). -1 + // indicates the rawTime is before the first transition and is handled gracefully by + // createOffsetInterval(). + final int initialTransitionIndex = findTransitionIndex(zoneInfo, rawTimeSeconds); + + if (isDst < 0) { + // This is treated as a special case to get it out of the way: + // When a caller has set isDst == -1 it means we can return the first match for + // the wall time we find. If the caller has specified a wall time that cannot + // exist this always returns -1. + + Integer result = doWallTimeSearch(zoneInfo, initialTransitionIndex, + wallTimeSeconds, true /* mustMatchDst */); + return result == null ? -1 : result; + } + + // If the wall time asserts a DST (isDst == 0 or 1) the search is performed twice: + // 1) The first attempts to find a DST offset that matches isDst exactly. + // 2) If it fails, isDst is assumed to be incorrect and adjustments are made to see + // if a valid wall time can be created. The result can be somewhat arbitrary. + + Integer result = doWallTimeSearch(zoneInfo, initialTransitionIndex, wallTimeSeconds, + true /* mustMatchDst */); + if (result == null) { + result = doWallTimeSearch(zoneInfo, initialTransitionIndex, wallTimeSeconds, + false /* mustMatchDst */); + } + if (result == null) { + result = -1; + } + return result; + } catch (CheckedArithmeticException e) { + return -1; + } + } + + /** + * Attempt to apply DST adjustments to {@code oldWallTimeSeconds} to create a wall time in + * {@code targetInterval}. + * + * <p>This is used when a caller has made an assertion about standard time / DST that cannot + * be matched to any offset interval that exists. We must therefore assume that the isDst + * assertion is incorrect and the invalid wall time is the result of some modification the + * caller made to a valid wall time that pushed them outside of the offset interval they + * were in. We must correct for any DST change that should have been applied when they did + * so. + * + * <p>Unfortunately, we have no information about what adjustment they made and so cannot + * know which offset interval they were previously in. For example, they may have added a + * second or a year to a valid time to arrive at what they have. + * + * <p>We try all offset types that are not the same as the isDst the caller asserted. For + * each possible offset we work out the offset difference between that and + * {@code targetInterval}, apply it, and see if we are still in {@code targetInterval}. If + * we are, then we have found an adjustment. + */ + private Integer tryOffsetAdjustments(ZoneInfo zoneInfo, int oldWallTimeSeconds, + OffsetInterval targetInterval, int transitionIndex, int isDstToFind) + throws CheckedArithmeticException { + + int[] offsetsToTry = getOffsetsOfType(zoneInfo, transitionIndex, isDstToFind); + for (int j = 0; j < offsetsToTry.length; j++) { + int rawOffsetSeconds = zoneInfo.mRawOffset / 1000; + int jOffsetSeconds = rawOffsetSeconds + offsetsToTry[j]; + int targetIntervalOffsetSeconds = targetInterval.getTotalOffsetSeconds(); + int adjustmentSeconds = targetIntervalOffsetSeconds - jOffsetSeconds; + int adjustedWallTimeSeconds = checkedAdd(oldWallTimeSeconds, adjustmentSeconds); + if (targetInterval.containsWallTime(adjustedWallTimeSeconds)) { + // Perform any arithmetic that might overflow. + int returnValue = checkedSubtract(adjustedWallTimeSeconds, + targetIntervalOffsetSeconds); + + // Modify field state and return the result. + calendar.setTimeInMillis(adjustedWallTimeSeconds * 1000L); + copyFieldsFromCalendar(); + this.isDst = targetInterval.getIsDst(); + this.gmtOffsetSeconds = targetIntervalOffsetSeconds; + return returnValue; + } + } + return null; + } + + /** + * Return an array of offsets that have the requested {@code isDst} value. + * The {@code startIndex} is used as a starting point so transitions nearest + * to that index are returned first. + */ + private static int[] getOffsetsOfType(ZoneInfo zoneInfo, int startIndex, int isDst) { + // +1 to account for the synthetic transition we invent before the first recorded one. + int[] offsets = new int[zoneInfo.mOffsets.length + 1]; + boolean[] seen = new boolean[zoneInfo.mOffsets.length]; + int numFound = 0; + + int delta = 0; + boolean clampTop = false; + boolean clampBottom = false; + do { + // delta = { 1, -1, 2, -2, 3, -3...} + delta *= -1; + if (delta >= 0) { + delta++; + } + + int transitionIndex = startIndex + delta; + if (delta < 0 && transitionIndex < -1) { + clampBottom = true; + continue; + } else if (delta > 0 && transitionIndex >= zoneInfo.mTypes.length) { + clampTop = true; + continue; + } + + if (transitionIndex == -1) { + if (isDst == 0) { + // Synthesize a non-DST transition before the first transition we have + // data for. + offsets[numFound++] = 0; // offset of 0 from raw offset + } + continue; + } + byte type = zoneInfo.mTypes[transitionIndex]; + if (!seen[type]) { + if (zoneInfo.mIsDsts[type] == isDst) { + offsets[numFound++] = zoneInfo.mOffsets[type]; + } + seen[type] = true; + } + } while (!(clampTop && clampBottom)); + + int[] toReturn = new int[numFound]; + System.arraycopy(offsets, 0, toReturn, 0, numFound); + return toReturn; + } + + /** + * Find a time <em>in seconds</em> the same or close to {@code wallTimeSeconds} that + * satisfies {@code mustMatchDst}. The search begins around the timezone offset transition + * with {@code initialTransitionIndex}. + * + * <p>If {@code mustMatchDst} is {@code true} the method can only return times that + * use timezone offsets that satisfy the {@code this.isDst} requirements. + * If {@code this.isDst == -1} it means that any offset can be used. + * + * <p>If {@code mustMatchDst} is {@code false} any offset that covers the + * currently set time is acceptable. That is: if {@code this.isDst} == -1, any offset + * transition can be used, if it is 0 or 1 the offset used must match {@code this.isDst}. + * + * <p>Note: This method both uses and can modify field state. It returns the matching time + * in seconds if a match has been found and modifies fields, or it returns {@code null} and + * leaves the field state unmodified. + */ + private Integer doWallTimeSearch(ZoneInfo zoneInfo, int initialTransitionIndex, + int wallTimeSeconds, boolean mustMatchDst) throws CheckedArithmeticException { + + // The loop below starts at the initialTransitionIndex and radiates out from that point + // up to 24 hours in either direction by applying transitionIndexDelta to inspect + // adjacent transitions (0, -1, +1, -2, +2). 24 hours is used because we assume that no + // total offset from UTC is ever > 24 hours. clampTop and clampBottom are used to + // indicate whether the search has either searched > 24 hours or exhausted the + // transition data in that direction. The search stops when a match is found or if + // clampTop and clampBottom are both true. + // The match logic employed is determined by the mustMatchDst parameter. + final int MAX_SEARCH_SECONDS = 24 * 60 * 60; + boolean clampTop = false, clampBottom = false; + int loop = 0; + do { + // transitionIndexDelta = { 0, -1, 1, -2, 2,..} + int transitionIndexDelta = (loop + 1) / 2; + if (loop % 2 == 1) { + transitionIndexDelta *= -1; + } + loop++; + + // Only do any work in this iteration if we need to. + if (transitionIndexDelta > 0 && clampTop + || transitionIndexDelta < 0 && clampBottom) { + continue; + } + + // Obtain the OffsetInterval to use. + int currentTransitionIndex = initialTransitionIndex + transitionIndexDelta; + OffsetInterval offsetInterval = + OffsetInterval.create(zoneInfo, currentTransitionIndex); + if (offsetInterval == null) { + // No transition exists with the index we tried: Stop searching in the + // current direction. + clampTop |= (transitionIndexDelta > 0); + clampBottom |= (transitionIndexDelta < 0); + continue; + } + + // Match the wallTimeSeconds against the OffsetInterval. + if (mustMatchDst) { + // Work out if the interval contains the wall time the caller specified and + // matches their isDst value. + if (offsetInterval.containsWallTime(wallTimeSeconds)) { + if (this.isDst == -1 || offsetInterval.getIsDst() == this.isDst) { + // This always returns the first OffsetInterval it finds that matches + // the wall time and isDst requirements. If this.isDst == -1 this means + // the result might be a DST or a non-DST answer for wall times that can + // exist in two OffsetIntervals. + int totalOffsetSeconds = offsetInterval.getTotalOffsetSeconds(); + int returnValue = checkedSubtract(wallTimeSeconds, + totalOffsetSeconds); + + copyFieldsFromCalendar(); + this.isDst = offsetInterval.getIsDst(); + this.gmtOffsetSeconds = totalOffsetSeconds; + return returnValue; + } + } + } else { + // To retain similar behavior to the old native implementation: if the caller is + // asserting the same isDst value as the OffsetInterval we are looking at we do + // not try to find an adjustment from another OffsetInterval of the same isDst + // type. If you remove this you get different results in situations like a + // DST -> DST transition or STD -> STD transition that results in an interval of + // "skipped" wall time. For example: if 01:30 (DST) is invalid and between two + // DST intervals, and the caller has passed isDst == 1, this results in a -1 + // being returned. + if (isDst != offsetInterval.getIsDst()) { + final int isDstToFind = isDst; + Integer returnValue = tryOffsetAdjustments(zoneInfo, wallTimeSeconds, + offsetInterval, currentTransitionIndex, isDstToFind); + if (returnValue != null) { + return returnValue; + } + } + } + + // See if we can avoid another loop in the current direction. + if (transitionIndexDelta > 0) { + // If we are searching forward and the OffsetInterval we have ends + // > MAX_SEARCH_SECONDS after the wall time, we don't need to look any further + // forward. + boolean endSearch = offsetInterval.getEndWallTimeSeconds() - wallTimeSeconds + > MAX_SEARCH_SECONDS; + if (endSearch) { + clampTop = true; + } + } else if (transitionIndexDelta < 0) { + boolean endSearch = wallTimeSeconds - offsetInterval.getStartWallTimeSeconds() + >= MAX_SEARCH_SECONDS; + if (endSearch) { + // If we are searching backward and the OffsetInterval starts + // > MAX_SEARCH_SECONDS before the wall time, we don't need to look any + // further backwards. + clampBottom = true; + } + } + } while (!(clampTop && clampBottom)); + return null; + } + + public void setYear(int year) { + this.year = year; + } + + public void setMonth(int month) { + this.month = month; + } + + public void setMonthDay(int monthDay) { + this.monthDay = monthDay; + } + + public void setHour(int hour) { + this.hour = hour; + } + + public void setMinute(int minute) { + this.minute = minute; + } + + public void setSecond(int second) { + this.second = second; + } + + public void setWeekDay(int weekDay) { + this.weekDay = weekDay; + } + + public void setYearDay(int yearDay) { + this.yearDay = yearDay; + } + + public void setIsDst(int isDst) { + this.isDst = isDst; + } + + public void setGmtOffset(int gmtoff) { + this.gmtOffsetSeconds = gmtoff; + } + + public int getYear() { + return year; + } + + public int getMonth() { + return month; + } + + public int getMonthDay() { + return monthDay; + } + + public int getHour() { + return hour; + } + + public int getMinute() { + return minute; + } + + public int getSecond() { + return second; + } + + public int getWeekDay() { + return weekDay; + } + + public int getYearDay() { + return yearDay; + } + + public int getGmtOffset() { + return gmtOffsetSeconds; + } + + public int getIsDst() { + return isDst; + } + + private void copyFieldsToCalendar() { + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, month); + calendar.set(Calendar.DAY_OF_MONTH, monthDay); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + calendar.set(Calendar.SECOND, second); + } + + private void copyFieldsFromCalendar() { + year = calendar.get(Calendar.YEAR); + month = calendar.get(Calendar.MONTH); + monthDay = calendar.get(Calendar.DAY_OF_MONTH); + hour = calendar.get(Calendar.HOUR_OF_DAY); + minute = calendar.get(Calendar.MINUTE); + second = calendar.get(Calendar.SECOND); + + // Calendar uses Sunday == 1. Android Time uses Sunday = 0. + weekDay = calendar.get(Calendar.DAY_OF_WEEK) - 1; + // Calendar enumerates from 1, Android Time enumerates from 0. + yearDay = calendar.get(Calendar.DAY_OF_YEAR) - 1; + } + + /** + * Find the transition in the {@code timezone} in effect at {@code timeSeconds}. + * + * <p>Returns an index in the range -1..timeZone.mTransitions.length - 1. -1 is used to + * indicate the time is before the first transition. Other values are an index into + * timeZone.mTransitions. + */ + private static int findTransitionIndex(ZoneInfo timeZone, int timeSeconds) { + int matchingRawTransition = Arrays.binarySearch(timeZone.mTransitions, timeSeconds); + if (matchingRawTransition < 0) { + matchingRawTransition = ~matchingRawTransition - 1; + } + return matchingRawTransition; + } + } + + /** + * A wall-time representation of a timezone offset interval. + * + * <p>Wall-time means "as it would appear locally in the timezone in which it applies". + * For example in 2007: + * PST was a -8:00 offset that ran until Mar 11, 2:00 AM. + * PDT was a -7:00 offset and ran from Mar 11, 3:00 AM to Nov 4, 2:00 AM. + * PST was a -8:00 offset and ran from Nov 4, 1:00 AM. + * Crucially this means that there was a "gap" after PST when PDT started, and an overlap when + * PDT ended and PST began. + * + * <p>For convenience all wall-time values are represented as the number of seconds since the + * beginning of the Unix epoch <em>in UTC</em>. To convert from a wall-time to the actual time + * in the offset it is necessary to <em>subtract</em> the {@code totalOffsetSeconds}. + * For example: If the offset in PST is -07:00 hours, then: + * timeInPstSeconds = wallTimeUtcSeconds - offsetSeconds + * i.e. 13:00 UTC - (-07:00) = 20:00 UTC = 13:00 PST + */ + static class OffsetInterval { + + private final int startWallTimeSeconds; + private final int endWallTimeSeconds; + private final int isDst; + private final int totalOffsetSeconds; + + /** + * Creates an {@link OffsetInterval}. + * + * <p>If {@code transitionIndex} is -1, the transition is synthesized to be a non-DST offset + * that runs from the beginning of time until the first transition in {@code timeZone} and + * has an offset of {@code timezone.mRawOffset}. If {@code transitionIndex} is the last + * transition that transition is considered to run until the end of representable time. + * Otherwise, the information is extracted from {@code timeZone.mTransitions}, + * {@code timeZone.mOffsets} an {@code timeZone.mIsDsts}. + */ + public static OffsetInterval create(ZoneInfo timeZone, int transitionIndex) + throws CheckedArithmeticException { + + if (transitionIndex < -1 || transitionIndex >= timeZone.mTransitions.length) { + return null; + } + + int rawOffsetSeconds = timeZone.mRawOffset / 1000; + if (transitionIndex == -1) { + int endWallTimeSeconds = checkedAdd(timeZone.mTransitions[0], rawOffsetSeconds); + return new OffsetInterval(Integer.MIN_VALUE, endWallTimeSeconds, 0 /* isDst */, + rawOffsetSeconds); + } + + byte type = timeZone.mTypes[transitionIndex]; + int totalOffsetSeconds = timeZone.mOffsets[type] + rawOffsetSeconds; + int endWallTimeSeconds; + if (transitionIndex == timeZone.mTransitions.length - 1) { + // If this is the last transition, make up the end time. + endWallTimeSeconds = Integer.MAX_VALUE; + } else { + endWallTimeSeconds = checkedAdd(timeZone.mTransitions[transitionIndex + 1], + totalOffsetSeconds); + } + int isDst = timeZone.mIsDsts[type]; + int startWallTimeSeconds = + checkedAdd(timeZone.mTransitions[transitionIndex], totalOffsetSeconds); + return new OffsetInterval( + startWallTimeSeconds, endWallTimeSeconds, isDst, totalOffsetSeconds); + } + + private OffsetInterval(int startWallTimeSeconds, int endWallTimeSeconds, int isDst, + int totalOffsetSeconds) { + this.startWallTimeSeconds = startWallTimeSeconds; + this.endWallTimeSeconds = endWallTimeSeconds; + this.isDst = isDst; + this.totalOffsetSeconds = totalOffsetSeconds; + } + + public boolean containsWallTime(long wallTimeSeconds) { + return wallTimeSeconds >= startWallTimeSeconds && wallTimeSeconds < endWallTimeSeconds; + } + + public int getIsDst() { + return isDst; + } + + public int getTotalOffsetSeconds() { + return totalOffsetSeconds; + } + + public long getEndWallTimeSeconds() { + return endWallTimeSeconds; + } + + public long getStartWallTimeSeconds() { + return startWallTimeSeconds; + } + } + + /** + * An exception used to indicate an arithmetic overflow or underflow. + */ + private static class CheckedArithmeticException extends Exception { + } + + /** + * Calculate (a + b). + * + * @throws CheckedArithmeticException if overflow or underflow occurs + */ + private static int checkedAdd(int a, int b) throws CheckedArithmeticException { + // Adapted from Guava IntMath.checkedAdd(); + long result = (long) a + b; + if (result != (int) result) { + throw new CheckedArithmeticException(); + } + return (int) result; + } + + /** + * Calculate (a - b). + * + * @throws CheckedArithmeticException if overflow or underflow occurs + */ + private static int checkedSubtract(int a, int b) throws CheckedArithmeticException { + // Adapted from Guava IntMath.checkedSubtract(); + long result = (long) a - b; + if (result != (int) result) { + throw new CheckedArithmeticException(); + } + return (int) result; + } } diff --git a/luni/src/main/java/libcore/util/ZoneInfoDB.java b/luni/src/main/java/libcore/util/ZoneInfoDB.java index 74947a6..07aaf04 100644 --- a/luni/src/main/java/libcore/util/ZoneInfoDB.java +++ b/luni/src/main/java/libcore/util/ZoneInfoDB.java @@ -229,9 +229,9 @@ public final class ZoneInfoDB { } public ZoneInfo makeTimeZone(String id) throws IOException { - ZoneInfo zoneInfo = cache.get(id); - // The object from the cache is cloned because TimeZone / ZoneInfo are mutable. - return zoneInfo == null ? null : (ZoneInfo) zoneInfo.clone(); + ZoneInfo zoneInfo = cache.get(id); + // The object from the cache is cloned because TimeZone / ZoneInfo are mutable. + return zoneInfo == null ? null : (ZoneInfo) zoneInfo.clone(); } } diff --git a/luni/src/main/native/libcore_icu_ICU.cpp b/luni/src/main/native/libcore_icu_ICU.cpp index 163d19c..733bf38 100644 --- a/luni/src/main/native/libcore_icu_ICU.cpp +++ b/luni/src/main/native/libcore_icu_ICU.cpp @@ -116,56 +116,6 @@ static jstring ICU_getScript(JNIEnv* env, jclass, jstring javaLocaleName) { return env->NewStringUTF(icuLocale.locale().getScript()); } -static jstring ICU_localeForLanguageTag(JNIEnv* env, jclass, jstring languageTag, jboolean strict) { - ScopedUtfChars languageTagChars(env, languageTag); - if (languageTagChars.c_str() == NULL) { - return NULL; - } - - // Naively assume that in the average case, the size of - // the normalized language tag will be very nearly the same as the - // size of the input. This is generally true for language - // tags that are "simple" language-region-variant combinations - // that don't contain any grandfathered tags. - const size_t initialBufferSize = languageTagChars.size() + 32; - std::vector<char> buffer(initialBufferSize); - int32_t parsedLength = 0; - - UErrorCode status = U_ZERO_ERROR; - size_t outputLength = uloc_forLanguageTag(languageTagChars.c_str(), &buffer[0], - buffer.size(), &parsedLength, &status); - // Note that we always allocate 1 char more than ICU asks us for, - // so that we can cleanly assert that it didn't overflow after the - // second call to uloc_forLanguageTag. - if (status == U_STRING_NOT_TERMINATED_WARNING) { - const size_t unterminated_size = buffer.size(); - buffer.resize(unterminated_size + 1); - buffer[unterminated_size] = '\0'; - } else if (status == U_BUFFER_OVERFLOW_ERROR) { - buffer.resize(outputLength + 1); - status = U_ZERO_ERROR; - outputLength = uloc_forLanguageTag(languageTagChars.c_str(), &buffer[0], buffer.size(), - &parsedLength, &status); - } - - if (U_FAILURE(status) || outputLength >= buffer.size()) { - return NULL; - } - - // By default, ICU will ignore all subtags starting at the first unparseable - // or invalid subtag. Our "strict" mode is specified to throw an error if - // that happens. - // - // NOTE: The cast is safe because parsedLength can never be negative thanks - // to the check above. ICU does not document any negative return values for - // that field, but check for it anyway. - if ((strict == JNI_TRUE) && (static_cast<uint32_t>(parsedLength) != languageTagChars.size())) { - return NULL; - } - - return env->NewStringUTF(&buffer[0]); -} - static jint ICU_getCurrencyFractionDigits(JNIEnv* env, jclass, jstring javaCurrencyCode) { ScopedJavaUnicodeString currencyCode(env, javaCurrencyCode); if (!currencyCode.valid()) { @@ -820,7 +770,6 @@ static JNINativeMethod gMethods[] = { NATIVE_METHOD(ICU, getIcuVersion, "()Ljava/lang/String;"), NATIVE_METHOD(ICU, getScript, "(Ljava/lang/String;)Ljava/lang/String;"), NATIVE_METHOD(ICU, getUnicodeVersion, "()Ljava/lang/String;"), - NATIVE_METHOD(ICU, localeForLanguageTag, "(Ljava/lang/String;Z)Ljava/lang/String;"), NATIVE_METHOD(ICU, initLocaleDataNative, "(Ljava/lang/String;Llibcore/icu/LocaleData;)Z"), NATIVE_METHOD(ICU, setDefaultLocale, "(Ljava/lang/String;)V"), NATIVE_METHOD(ICU, toLowerCase, "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"), diff --git a/luni/src/test/java/libcore/icu/ICUTest.java b/luni/src/test/java/libcore/icu/ICUTest.java index 3fa1f46..525d372 100644 --- a/luni/src/test/java/libcore/icu/ICUTest.java +++ b/luni/src/test/java/libcore/icu/ICUTest.java @@ -198,12 +198,6 @@ public class ICUTest extends junit.framework.TestCase { Collator.getInstance(sr_Latn_BA); Collator.getInstance(sr_Latn_ME); - // TODO: This needs to be fixed. We shouldn't output attribute key - // expansions in the language tag or the toString output. The tests - // will fail with something like: - // - // expected:<de-u-co[-phonebk-kf-upper-kn]> but was: - // <de-u-co[lcasefirst-upper-collation-phonebook-colnumeric-yes]> Locale l = Locale.forLanguageTag("de-u-co-phonebk-kf-upper-kn"); assertEquals("de__#u-co-phonebk-kf-upper-kn", l.toString()); assertEquals("de-u-co-phonebk-kf-upper-kn", l.toLanguageTag()); diff --git a/luni/src/test/java/libcore/java/nio/BufferTest.java b/luni/src/test/java/libcore/java/nio/BufferTest.java index 613c6fa..c936cdf 100644 --- a/luni/src/test/java/libcore/java/nio/BufferTest.java +++ b/luni/src/test/java/libcore/java/nio/BufferTest.java @@ -20,6 +20,8 @@ import junit.framework.TestCase; import java.io.File; import java.io.RandomAccessFile; import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.nio.Buffer; import java.nio.BufferOverflowException; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; @@ -600,17 +602,31 @@ public class BufferTest extends TestCase { assertTrue(b.isDirect()); // Check the buffer has an array of the right size. assertTrue(b.hasArray()); - assertEquals(0, b.arrayOffset()); byte[] array = b.array(); - assertEquals(10, array.length); + assertTrue(array.length >= b.capacity()); + assertEquals(10, b.capacity()); // Check that writes to the array show up in the buffer. assertEquals(0, b.get(0)); - array[0] = 1; + array[b.arrayOffset()] = 1; assertEquals(1, b.get(0)); // Check that writes to the buffer show up in the array. - assertEquals(1, array[0]); + assertEquals(1, array[b.arrayOffset()]); b.put(0, (byte) 0); - assertEquals(0, array[0]); + assertEquals(0, array[b.arrayOffset()]); + } + + // Test that direct byte buffers are 8 byte aligned. + // http://b/16449607 + public void testDirectByteBufferAlignment() throws Exception { + ByteBuffer b = ByteBuffer.allocateDirect(10); + Field addressField = Buffer.class.getDeclaredField("effectiveDirectAddress"); + assertTrue(addressField != null); + addressField.setAccessible(true); + long address = addressField.getLong(b); + // Check that the address field is aligned by 8. + // Normally reading this field happens in native code by calling + // GetDirectBufferAddress. + assertEquals(0, address % 8); } public void testSliceOffset() throws Exception { @@ -618,14 +634,12 @@ public class BufferTest extends TestCase { ByteBuffer buffer = ByteBuffer.allocate(10); buffer.get(); ByteBuffer slice = buffer.slice(); - assertEquals(0, buffer.arrayOffset()); - assertEquals(1, slice.arrayOffset()); + assertEquals(buffer.arrayOffset() + 1, slice.arrayOffset()); ByteBuffer directBuffer = ByteBuffer.allocateDirect(10); directBuffer.get(); ByteBuffer directSlice = directBuffer.slice(); - assertEquals(0, directBuffer.arrayOffset()); - assertEquals(1, directSlice.arrayOffset()); + assertEquals(directBuffer.arrayOffset() + 1, directSlice.arrayOffset()); } // http://code.google.com/p/android/issues/detail?id=16184 diff --git a/luni/src/test/java/libcore/java/util/LocaleTest.java b/luni/src/test/java/libcore/java/util/LocaleTest.java index 94bf363..23a4f28 100644 --- a/luni/src/test/java/libcore/java/util/LocaleTest.java +++ b/luni/src/test/java/libcore/java/util/LocaleTest.java @@ -541,13 +541,6 @@ public class LocaleTest extends junit.framework.TestCase { assertEquals("eng", l.getLanguage()); assertEquals("419", l.getCountry()); - // IND is an invalid region code so ICU helpfully tries to parse it as - // a 3 letter language code, even if it isn't a valid ISO-639-3 code - // either. - l = fromLanguageTag("en-USB", useBuilder); - assertEquals("usb", l.getLanguage()); - assertEquals("", l.getCountry()); - // Script tags shouldn't be mis-recognized as regions. l = fromLanguageTag("en-Latn", useBuilder); assertEquals("en", l.getLanguage()); @@ -612,16 +605,24 @@ public class LocaleTest extends junit.framework.TestCase { } catch (IllformedLocaleException expected) { } - // Ill-formed extension with long subtag. + // Two extension keys in a row (i.e, another case of an ill-formed + // empty exception). try { - fromLanguageTag("en-f-fooobaaaz", true); + fromLanguageTag("en-f-g-fo-baar", true); fail(); } catch (IllformedLocaleException expected) { } - // Ill-formed extension key. + // Dangling empty key after a well formed extension. try { - fromLanguageTag("en-9-baa", true); + fromLanguageTag("en-f-fo-baar-g", true); + fail(); + } catch (IllformedLocaleException expected) { + } + + // Ill-formed extension with long subtag. + try { + fromLanguageTag("en-f-fooobaaaz", true); fail(); } catch (IllformedLocaleException expected) { } @@ -700,7 +701,7 @@ public class LocaleTest extends junit.framework.TestCase { assertEquals("en", l.getLanguage()); assertEquals("Latn", l.getScript()); assertEquals("GB", l.getCountry()); - assertEquals("FOOOO_POSIX", l.getVariant()); + assertEquals("FOOOO", l.getVariant()); assertEquals("fo-bar-baaz", l.getExtension('g')); // Multiple extensions @@ -708,7 +709,7 @@ public class LocaleTest extends junit.framework.TestCase { assertEquals("en", l.getLanguage()); assertEquals("Latn", l.getScript()); assertEquals("US", l.getCountry()); - assertEquals("FOOOO_POSIX", l.getVariant()); + assertEquals("FOOOO", l.getVariant()); assertEquals("fo-bar", l.getExtension('g')); assertEquals("go-gaz", l.getExtension('h')); @@ -738,6 +739,13 @@ public class LocaleTest extends junit.framework.TestCase { assertEquals("", l.getScript()); assertEquals("", l.getCountry()); assertEquals("fo", l.getExtension('f')); + + l = fromLanguageTag("en-f-fo-x-a-b-c-d-e-fo", useBuilder); + assertEquals("en", l.getLanguage()); + assertEquals("", l.getScript()); + assertEquals("", l.getCountry()); + assertEquals("fo", l.getExtension('f')); + assertEquals("a-b-c-d-e-fo", l.getExtension('x')); } public void test_forLanguageTag() { @@ -782,7 +790,7 @@ public class LocaleTest extends junit.framework.TestCase { public void test_setLanguageTag_malformedTags() { Locale l = fromLanguageTag("a", false); - assertEquals("", l.getLanguage()); + assertEquals("und", l.getLanguage()); assertEquals("", l.getCountry()); assertEquals("", l.getVariant()); assertEquals("", l.getScript()); @@ -1113,4 +1121,22 @@ public class LocaleTest extends junit.framework.TestCase { .build(); assertEquals("en-US-POSIX", posix.toLanguageTag()); } + + public void test_forLanguageTag_grandFatheredLocale() { + // Regular grandfathered locale. + Locale gaulish = Locale.forLanguageTag("cel-gaulish"); + assertEquals("xtg", gaulish.getLanguage()); + assertEquals("cel-gaulish", gaulish.getExtension(Locale.PRIVATE_USE_EXTENSION)); + assertEquals("", gaulish.getCountry()); + assertEquals("", gaulish.getScript()); + assertEquals("", gaulish.getVariant()); + + // Irregular grandfathered locale. + Locale enochian = Locale.forLanguageTag("i-enochian"); + assertEquals("und", enochian.getLanguage()); + assertEquals("i-enochian", enochian.getExtension(Locale.PRIVATE_USE_EXTENSION)); + assertEquals("", enochian.getCountry()); + assertEquals("", enochian.getScript()); + assertEquals("", enochian.getVariant()); + } } |