ICU-8078 Better ULocale/Locale mapping on JRE 7+ using the new Locale APIs.

X-SVN-Rev: 29181
This commit is contained in:
Yoshito Umaoka 2010-12-09 21:55:20 +00:00
parent 4f5faa01b7
commit 1e3205b869
3 changed files with 436 additions and 61 deletions

View File

@ -8,14 +8,18 @@
package com.ibm.icu.util;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.ParseException;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.MissingResourceException;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import com.ibm.icu.impl.ICUCache;
import com.ibm.icu.impl.ICUResourceBundle;
@ -241,7 +245,7 @@ public final class ULocale implements Serializable {
// default empty locale
private static final Locale EMPTY_LOCALE = new Locale("", "");
// specia keyword key for Unicode locale attributes
// special keyword key for Unicode locale attributes
private static final String LOCALE_ATTRIBUTE_KEY = "attribute";
/**
@ -360,21 +364,6 @@ public final class ULocale implements Serializable {
}
}
/*
* This table is used for mapping between ICU and special Java
* locales. When an ICU locale matches <minumum base> with
* <keyword>/<value>, the ICU locale is mapped to <Java> locale.
* For example, both ja_JP@calendar=japanese and ja@calendar=japanese
* are mapped to Java locale "ja_JP_JP". ICU locale "nn" is mapped
* to Java locale "no_NO_NY".
*/
private static final String[][] _javaLocaleMap = {
// { <Java>, <ICU base>, <keyword>, <value>, <minimum base>
{ "ja_JP_JP", "ja_JP", "calendar", "japanese", "ja"},
{ "no_NO_NY", "nn_NO", null, null, "nn"},
{ "th_TH_TH", "th_TH", "numbers", "thai", "th"},
};
/**
* Private constructor used by static initializers.
*/
@ -405,22 +394,9 @@ public final class ULocale implements Serializable {
ULocale result = CACHE.get(loc);
if (result == null) {
if (defaultULocale != null && loc == defaultULocale.locale) {
result = defaultULocale;
} else {
String locStr = loc.toString();
if (locStr.length() == 0) {
result = ROOT;
} else {
for (int i = 0; i < _javaLocaleMap.length; i++) {
if (_javaLocaleMap[i][0].equals(locStr)) {
LocaleIDParser p = new LocaleIDParser(_javaLocaleMap[i][1]);
p.setKeywordValue(_javaLocaleMap[i][2], _javaLocaleMap[i][3]);
locStr = p.getName();
break;
}
}
result = new ULocale(locStr, loc);
}
result = defaultULocale;
} else {
result = JDKLocaleMapper.INSTANCE.toULocale(loc);
}
CACHE.put(loc, result);
}
@ -527,24 +503,7 @@ public final class ULocale implements Serializable {
*/
public Locale toLocale() {
if (locale == null) {
LocaleIDParser p = new LocaleIDParser(localeID);
String base = p.getBaseName();
for (int i = 0; i < _javaLocaleMap.length; i++) {
if (base.equals(_javaLocaleMap[i][1]) || base.equals(_javaLocaleMap[i][4])) {
if (_javaLocaleMap[i][2] != null) {
String val = p.getKeywordValue(_javaLocaleMap[i][2]);
if (val != null && val.equals(_javaLocaleMap[i][3])) {
p = new LocaleIDParser(_javaLocaleMap[i][0]);
break;
}
} else {
p = new LocaleIDParser(_javaLocaleMap[i][0]);
break;
}
}
}
String[] names = p.getLanguageScriptCountryVariant();
locale = new Locale(names[0], names[2], names[3]);
locale = JDKLocaleMapper.INSTANCE.toLocale(this);
}
return locale;
}
@ -3545,4 +3504,283 @@ public final class ULocale implements Serializable {
}
return type;
}
/*
* JDK Locale Mapper
*/
private static final class JDKLocaleMapper {
public static final JDKLocaleMapper INSTANCE = new JDKLocaleMapper();
private static boolean isJava7orNewer = false;
/*
* New methods in Java 7 Locale class
*/
private static Method mGetScript;
private static Method mGetExtensionKeys;
private static Method mGetExtension;
private static Method mGetUnicodeLocaleKeys;
private static Method mGetUnicodeLocaleAttributes;
private static Method mGetUnicodeLocaleType;
private static Method mForLanguageTag;
/*
* This table is used for mapping between ICU and special Java
* 6 locales. When an ICU locale matches <minumum base> with
* <keyword>/<value>, the ICU locale is mapped to <Java> locale.
* For example, both ja_JP@calendar=japanese and ja@calendar=japanese
* are mapped to Java locale "ja_JP_JP". ICU locale "nn" is mapped
* to Java locale "no_NO_NY".
*/
private static final String[][] JAVA6_MAPDATA = {
// { <Java>, <ICU base>, <keyword>, <value>, <minimum base>
{ "ja_JP_JP", "ja_JP", "calendar", "japanese", "ja"},
{ "no_NO_NY", "nn_NO", null, null, "nn"},
{ "th_TH_TH", "th_TH", "numbers", "thai", "th"},
};
static {
try {
mGetScript = Locale.class.getMethod("getScript", (Class[]) null);
mGetExtensionKeys = Locale.class.getMethod("getExtensionKeys", (Class[]) null);
mGetExtension = Locale.class.getMethod("getExtension", char.class);
mGetUnicodeLocaleKeys = Locale.class.getMethod("getUnicodeLocaleKeys", (Class[]) null);
mGetUnicodeLocaleAttributes = Locale.class.getMethod("getUnicodeLocaleAttributes", (Class[]) null);
mGetUnicodeLocaleType = Locale.class.getMethod("getUnicodeLocaleType", String.class);
mForLanguageTag = Locale.class.getMethod("forLanguageTag", String.class);
isJava7orNewer = true;
} catch (NoSuchMethodException e) {
// Java 6 or older
}
}
private JDKLocaleMapper() {
}
public ULocale toULocale(Locale loc) {
return isJava7orNewer ? toULocale7(loc) : toULocale6(loc);
}
public Locale toLocale(ULocale uloc) {
return isJava7orNewer ? toLocale7(uloc) : toLocale6(uloc);
}
private ULocale toULocale7(Locale loc) {
String language = loc.getLanguage();
String script = "";
String country = loc.getCountry();
String variant = loc.getVariant();
Set<String> attributes = null;
Map<String, String> keywords = null;
try {
script = (String) mGetScript.invoke(loc, (Object[]) null);
@SuppressWarnings("unchecked")
Set<Character> extKeys = (Set<Character>) mGetExtensionKeys.invoke(loc, (Object[]) null);
if (!extKeys.isEmpty()) {
for (Character extKey : extKeys) {
if (extKey.charValue() == 'u') {
// Found Unicode locale extension
// attributes
@SuppressWarnings("unchecked")
Set<String> uAttributes = (Set<String>) mGetUnicodeLocaleAttributes.invoke(loc, (Object[]) null);
if (!uAttributes.isEmpty()) {
attributes = new TreeSet<String>();
for (String attr : uAttributes) {
attributes.add(attr);
}
}
// keywords
@SuppressWarnings("unchecked")
Set<String> uKeys = (Set<String>) mGetUnicodeLocaleKeys.invoke(loc, (Object[]) null);
for (String kwKey : uKeys) {
String kwVal = (String) mGetUnicodeLocaleType.invoke(loc, kwKey);
if (kwVal != null) {
if (kwKey.equals("va")) {
// va-* is interpreted as a variant
variant = (variant.length() == 0) ? kwVal : kwVal + "_" + variant;
} else {
if (keywords == null) {
keywords = new TreeMap<String, String>();
}
keywords.put(kwKey, kwVal);
}
}
}
} else {
String extVal = (String) mGetExtension.invoke(loc, extKey);
if (extVal != null) {
if (keywords == null) {
keywords = new TreeMap<String, String>();
}
keywords.put(String.valueOf(extKey), extVal);
}
}
}
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
// JDK locale no_NO_NY is not interpreted as Nynorsk by ICU,
// and it should be transformed to nn_NO.
// Note: JDK7+ unerstand both no_NO_NY and nn_NO. When convert
// ICU locale to JDK, we do not need to map nn_NO back to no_NO_NY.
if (language.equals("no") && country.equals("NO") && variant.equals("NY")) {
language = "nn";
variant = "";
}
// Constructing ID
StringBuilder buf = new StringBuilder(language);
if (script.length() > 0) {
buf.append('_');
buf.append(script);
}
if (country.length() > 0) {
buf.append('_');
buf.append(country);
}
if (variant.length() > 0) {
if (country.length() == 0) {
buf.append('_');
}
buf.append('_');
buf.append(variant);
}
if (attributes != null) {
// transform Unicode attributes into a keyword
StringBuilder attrBuf = new StringBuilder();
for (String attr : attributes) {
if (attrBuf.length() != 0) {
attrBuf.append('-');
}
attrBuf.append(attr);
}
if (keywords == null) {
keywords = new TreeMap<String, String>();
}
keywords.put(LOCALE_ATTRIBUTE_KEY, attrBuf.toString());
}
if (keywords != null) {
buf.append('@');
boolean addSep = false;
for (Entry<String, String> kwEntry : keywords.entrySet()) {
String kwKey = kwEntry.getKey();
String kwVal = kwEntry.getValue();
if (kwKey.length() != 1) {
// Unicode locale key
kwKey = bcp47ToLDMLKey(kwKey);
// use "true" as the value of typeless keywords
kwVal = bcp47ToLDMLType(kwKey, ((kwVal.length() == 0) ? "true" : kwVal));
}
if (addSep) {
buf.append(';');
} else {
addSep = true;
}
buf.append(kwKey);
buf.append('=');
buf.append(kwVal);
}
}
return new ULocale(buf.toString());
}
private ULocale toULocale6(Locale loc) {
ULocale uloc = null;
String locStr = loc.toString();
if (locStr.length() == 0) {
uloc = ULocale.ROOT;
} else {
for (int i = 0; i < JAVA6_MAPDATA.length; i++) {
if (JAVA6_MAPDATA[i][0].equals(locStr)) {
LocaleIDParser p = new LocaleIDParser(JAVA6_MAPDATA[i][1]);
p.setKeywordValue(JAVA6_MAPDATA[i][2], JAVA6_MAPDATA[i][3]);
locStr = p.getName();
break;
}
}
uloc = new ULocale(locStr, loc);
}
return uloc;
}
private Locale toLocale7(ULocale uloc) {
Locale loc = null;
String ulocStr = uloc.getName();
if (uloc.getScript().length() > 0 || ulocStr.contains("@")) {
// With script or keywords available, the best way
// to get a mapped Locale is to go through a language tag.
// A Locale with script or keywords can only have variants
// that is 1 to 8 alphanum. If this ULocale has a variant
// subtag not satisfying the criteria, the variant subtag
// will be lost.
String tag = uloc.toLanguageTag();
// Workaround for variant casing problem:
//
// The variant field in ICU is case insensitive and normalized
// to upper case letters by getVariant(), while
// the variant field in JDK Locale is case sensitive.
// ULocale#toLanguageTag use lower case characters for
// BCP 47 variant and private use x-lvariant.
//
// Locale#forLanguageTag in JDK preserves character casing
// for variant. Because ICU always normalizes variant to
// upper case, we convert language tag to upper case here.
tag = AsciiUtil.toUpperString(tag);
try {
loc = (Locale)mForLanguageTag.invoke(null, tag);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
if (loc == null) {
// Without script or keywords, use a Locale constructor,
// so we can preserve any ill-formed variants.
loc = new Locale(uloc.getLanguage(), uloc.getCountry(), uloc.getVariant());
}
return loc;
}
private Locale toLocale6(ULocale uloc) {
String locstr = uloc.getBaseName();
for (int i = 0; i < JAVA6_MAPDATA.length; i++) {
if (locstr.equals(JAVA6_MAPDATA[i][1]) || locstr.equals(JAVA6_MAPDATA[i][4])) {
if (JAVA6_MAPDATA[i][2] != null) {
String val = uloc.getKeywordValue(JAVA6_MAPDATA[i][2]);
if (val != null && val.equals(JAVA6_MAPDATA[i][3])) {
locstr = JAVA6_MAPDATA[i][0];
break;
}
} else {
locstr = JAVA6_MAPDATA[i][0];
break;
}
}
}
LocaleIDParser p = new LocaleIDParser(locstr);
String[] names = p.getLanguageScriptCountryVariant();
return new Locale(names[0], names[2], names[3]);
}
}
}

View File

@ -26,6 +26,7 @@ import com.ibm.icu.text.DateFormat;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.VersionInfo;
public class IntlTestDateFormatAPI extends com.ibm.icu.dev.test.TestFmwk
{
@ -152,6 +153,7 @@ public class IntlTestDateFormatAPI extends com.ibm.icu.dev.test.TestFmwk
// Ticket#6280
// These locales should be included in the result
boolean java7orLater = (VersionInfo.javaVersion().compareTo(VersionInfo.getInstance(1, 7)) >= 0);
final Locale[] samples = {
new Locale("zh", "CN"),
new Locale("zh", "TW"),
@ -172,7 +174,13 @@ public class IntlTestDateFormatAPI extends com.ibm.icu.dev.test.TestFmwk
}
for (int i = 0; i < available.length; i++) {
if (!available[i]) {
errln("ERROR: missing Locale: " + samples[i]);
if (java7orLater) {
// Java 7 supports script field, so zh_Hans_CN is included
// in the available locale list.
logln("INFO: missing Locale: " + samples[i]);
} else {
errln("ERROR: missing Locale: " + samples[i]);
}
}
}

View File

@ -38,6 +38,8 @@ import com.ibm.icu.util.VersionInfo;
public class ULocaleTest extends TestFmwk {
private static final boolean JAVA7_OR_LATER = (VersionInfo.javaVersion().compareTo(VersionInfo.getInstance(1, 7)) >= 0);
public static void main(String[] args) throws Exception {
new ULocaleTest().run(args);
}
@ -191,7 +193,7 @@ public class ULocaleTest extends TestFmwk {
*/
public void TestJavaLocaleCompatibility() {
Locale backupDefault = Locale.getDefault();
// Java Locale for ja_JP with Japanese calendar
Locale jaJPJP = new Locale("ja", "JP", "JP");
Locale jaJP = new Locale("ja", "JP");
@ -213,8 +215,14 @@ public class ULocaleTest extends TestFmwk {
// Default locale
Locale.setDefault(jaJPJP);
ULocale defUloc = ULocale.getDefault();
if (!defUloc.toString().equals("ja_JP@calendar=japanese")) {
errln("FAIL: Invalid default ULocale: " + defUloc + " /expected: ja_JP@calendar=japanese");
if (JAVA7_OR_LATER) {
if (!defUloc.toString().equals("ja_JP_JP@calendar=japanese")) {
errln("FAIL: Invalid default ULocale: " + defUloc + " /expected: ja_JP_JP@calendar=japanese");
}
} else {
if (!defUloc.toString().equals("ja_JP@calendar=japanese")) {
errln("FAIL: Invalid default ULocale: " + defUloc + " /expected: ja_JP@calendar=japanese");
}
}
// Check calendar type
cal = Calendar.getInstance();
@ -227,7 +235,7 @@ public class ULocaleTest extends TestFmwk {
// Set default via ULocale
ULocale ujaJP_calJP = new ULocale("ja_JP@calendar=japanese");
ULocale.setDefault(ujaJP_calJP);
if (!Locale.getDefault().equals(jaJPJP)) {
if (!JAVA7_OR_LATER && !Locale.getDefault().equals(jaJPJP)) {
errln("FAIL: ULocale#setDefault failed to set Java Locale ja_JP_JP /actual: " + Locale.getDefault());
}
// Ticket#6672 - missing keywords
@ -246,7 +254,7 @@ public class ULocaleTest extends TestFmwk {
// We also want to map ICU locale ja@calendar=japanese to Java ja_JP_JP
ULocale.setDefault(new ULocale("ja@calendar=japanese"));
if (!Locale.getDefault().equals(jaJPJP)) {
if (!JAVA7_OR_LATER && !Locale.getDefault().equals(jaJPJP)) {
errln("FAIL: ULocale#setDefault failed to set Java Locale ja_JP_JP /actual: " + Locale.getDefault());
}
Locale.setDefault(backupDefault);
@ -255,28 +263,28 @@ public class ULocaleTest extends TestFmwk {
Locale noNONY = new Locale("no", "NO", "NY");
Locale.setDefault(noNONY);
defUloc = ULocale.getDefault();
if (defUloc.toString().equals("nn_NY")) {
errln("FAIL: Invalid default ULocale: " + defUloc + " /expected: nn_NY");
if (!defUloc.toString().equals("nn_NO")) {
errln("FAIL: Invalid default ULocale: " + defUloc + " /expected: nn_NO");
}
Locale.setDefault(backupDefault);
// Java th_TH_TH -> ICU th_TH@numbers=thai
ULocale.setDefault(new ULocale("th@numbers=thai"));
if (!Locale.getDefault().equals(thTHTH)) {
if (!JAVA7_OR_LATER && !Locale.getDefault().equals(thTHTH)) {
errln("FAIL: ULocale#setDefault failed to set Java Locale th_TH_TH /actual: " + Locale.getDefault());
}
Locale.setDefault(backupDefault);
// Set default via ULocale
ULocale.setDefault(new ULocale("nn_NO"));
if (!Locale.getDefault().equals(noNONY)) {
if (!JAVA7_OR_LATER && !Locale.getDefault().equals(noNONY)) {
errln("FAIL: ULocale#setDefault failed to set Java Locale no_NO_NY /actual: " + Locale.getDefault());
}
Locale.setDefault(backupDefault);
// We also want to map ICU locale nn to Java no_NO_NY
ULocale.setDefault(new ULocale("nn"));
if (!Locale.getDefault().equals(noNONY)) {
if (!JAVA7_OR_LATER && !Locale.getDefault().equals(noNONY)) {
errln("FAIL: ULocale#setDefault failed to set Java Locale no_NO_NY /actual: " + Locale.getDefault());
}
Locale.setDefault(backupDefault);
@ -4021,4 +4029,125 @@ public class ULocaleTest extends TestFmwk {
errln("getUnicodeLocaleType must throw an exception on illegal input key");
}
}
public void TestForLocale() {
Object[][] DATA = {
{new Locale(""), ""},
{new Locale("en", "US"), "en_US"},
{new Locale("en", "US", "POSIX"), "en_US_POSIX"},
{new Locale("", "US"), "_US"},
{new Locale("en", "", "POSIX"), "en__POSIX"},
{new Locale("no", "NO", "NY"), "nn_NO"},
};
for (int i = 0; i < DATA.length; i++) {
ULocale uloc = ULocale.forLocale((Locale) DATA[i][0]);
assertEquals("forLocale with " + DATA[i][0], DATA[i][1], uloc.getName());
}
if (JAVA7_OR_LATER) {
Object[][] DATA7 = {
{new Locale("ja", "JP", "JP"), "ja_JP_JP@calendar=japanese"},
{new Locale("th", "TH", "TH"), "th_TH_TH@numbers=thai"},
};
for (int i = 0; i < DATA7.length; i++) {
ULocale uloc = ULocale.forLocale((Locale) DATA7[i][0]);
assertEquals("forLocale with " + DATA7[i][0], DATA7[i][1], uloc.getName());
}
try {
Method localeForLanguageTag = Locale.class.getMethod("forLanguageTag", String.class);
String[][] DATA7EXT = {
{"en-Latn-US", "en_Latn_US"},
{"zh-Hant-TW", "zh_Hant_TW"},
{"und-US-u-cu-usd", "_US@currency=usd"},
{"th-TH-u-ca-buddhist-nu-thai", "th_TH@calendar=buddhist;numbers=thai"},
{"en-US-u-va-POSIX", "en_US_POSIX"},
{"de-DE-u-co-phonebk", "de_DE@collation=phonebook"},
{"en-a-exta-b-extb-x-privu", "en@a=exta;b=extb;x=privu"},
{"fr-u-attr1-attr2-cu-eur", "fr@attribute=attr1-attr2;currency=eur"},
};
for (int i = 0; i < DATA7EXT.length; i++) {
Locale loc = (Locale) localeForLanguageTag.invoke(null, DATA7EXT[i][0]);
ULocale uloc = ULocale.forLocale(loc);
assertEquals("forLocale with " + loc, DATA7EXT[i][1], uloc.getName());
}
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
Object[][] DATA6 = {
{new Locale("ja", "JP", "JP"), "ja_JP@calendar=japanese"},
{new Locale("th", "TH", "TH"), "th_TH@numbers=thai"},
};
for (int i = 0; i < DATA6.length; i++) {
ULocale uloc = ULocale.forLocale((Locale) DATA6[i][0]);
assertEquals("forLocale with " + DATA6[i][0], DATA6[i][1], uloc.getName());
}
}
}
public void TestToLocale() {
Object[][] DATA = {
{"", new Locale("")},
{"en_US", new Locale("en", "US")},
{"_US", new Locale("", "US")},
{"en__POSIX", new Locale("en", "", "POSIX")},
};
for (int i = 0; i < DATA.length; i++) {
Locale loc = new ULocale((String) DATA[i][0]).toLocale();
assertEquals("toLocale with " + DATA[i][0], DATA[i][1], loc);
}
if (JAVA7_OR_LATER) {
Object[][] DATA7 = {
{"nn_NO", new Locale("nn", "NO")},
{"no_NO_NY", new Locale("no", "NO", "NY")},
};
for (int i = 0; i < DATA7.length; i++) {
Locale loc = new ULocale((String) DATA7[i][0]).toLocale();
assertEquals("toLocale with " + DATA7[i][0], DATA7[i][1], loc);
}
try {
Method localeForLanguageTag = Locale.class.getMethod("forLanguageTag", String.class);
String[][] DATA7EXT = {
{"en_Latn_US", "en-Latn-US"},
{"zh_Hant_TW", "zh-Hant-TW"},
{"ja_JP@calendar=japanese", "ja-JP-u-ca-japanese"},
{"ja_JP_JP@calendar=japanese", "ja-JP-u-ca-japanese-x-lvariant-JP"},
{"th_TH@numbers=thai", "th-TH-u-nu-thai"},
{"th_TH_TH@numbers=thai", "th-TH-u-nu-thai-x-lvariant-TH"},
{"de@collation=phonebook", "de-u-co-phonebk"},
{"en@a=exta;b=extb;x=privu", "en-a-exta-b-extb-x-privu"},
{"fr@attribute=attr1-attr2;currency=eur", "fr-u-attr1-attr2-cu-eur"},
};
for (int i = 0; i < DATA7EXT.length; i++) {
Locale loc = new ULocale((String) DATA7EXT[i][0]).toLocale();
Locale expected = (Locale) localeForLanguageTag.invoke(null, DATA7EXT[i][1]);
assertEquals("toLocale with " + DATA7EXT[i][0], expected, loc);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
Object[][] DATA6 = {
{"nn_NO", new Locale("no", "NO", "NY")},
{"no_NO_NY", new Locale("no", "NO", "NY")},
{"ja_JP@calendar=japanese", new Locale("ja", "JP", "JP")},
{"th_TH@numbers=thai", new Locale("th", "TH", "TH")},
};
for (int i = 0; i < DATA6.length; i++) {
Locale loc = new ULocale((String) DATA6[i][0]).toLocale();
assertEquals("toLocale with " + DATA6[i][0], DATA6[i][1], loc);
}
}
}
}