/* * Copyright (C) 2008 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.ibm.icu4jni.util; import java.util.Arrays; import java.util.Enumeration; import java.util.ListResourceBundle; import java.util.Locale; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.TimeZone; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Logger; /** * Makes ICU data accessible to Java. * * TODO: move the LocaleData stuff into LocaleData and rename this class. */ public class Resources { // A cache for the locale-specific data. private static final ConcurrentHashMap<String, LocaleData> localeDataCache = new ConcurrentHashMap<String, LocaleData>(); /** * Cache for ISO language names. */ private static String[] isoLanguages = null; /** * Cache for ISO country names. */ private static String[] isoCountries = null; /** * Available locales cache. */ private static String[] availableLocales = null; /** * Available timezones cache. */ private static String[] availableTimezones = null; /** * Returns a shared LocaleData for the given locale. */ public static LocaleData getLocaleData(Locale locale) { if (locale == null) { locale = Locale.getDefault(); } String localeName = locale.toString(); LocaleData localeData = localeDataCache.get(localeName); if (localeData != null) { return localeData; } localeData = makeLocaleData(locale); boolean absent = (localeDataCache.putIfAbsent(localeName, localeData) == null); return absent ? localeData : localeDataCache.get(localeName); } private static LocaleData makeLocaleData(Locale locale) { String language = locale.getLanguage(); String country = locale.getCountry(); String variant = locale.getVariant(); // Start with data from the parent (next-most-specific) locale... LocaleData result = new LocaleData(); if (variant.length() > 0) { result.overrideWithDataFrom(getLocaleData(new Locale(language, country, ""))); } else if (country.length() > 0) { result.overrideWithDataFrom(getLocaleData(new Locale(language, "", ""))); } else if (language.length() > 0) { result.overrideWithDataFrom(getLocaleData(new Locale("", "", ""))); } // Override with data from this locale. result.overrideWithDataFrom(initLocaleData(locale)); return result; } /** * Returns an array of ISO language names (two-letter codes), fetched either * from ICU's database or from our memory cache. * * @return The array. */ public static String[] getISOLanguages() { if (isoLanguages == null) { isoLanguages = getISOLanguagesNative(); } return isoLanguages.clone(); } /** * Returns an array of ISO country names (two-letter codes), fetched either * from ICU's database or from our memory cache. * * @return The array. */ public static String[] getISOCountries() { if (isoCountries == null) { isoCountries = getISOCountriesNative(); } return isoCountries.clone(); } /** * Returns an array of names of locales that are available in the system, * fetched either from ICU's database or from our memory cache. * * @return The array. */ public static String[] getAvailableLocales() { if (availableLocales == null) { availableLocales = getAvailableLocalesNative(); } return availableLocales.clone(); } /** * Returns an array of names of timezones that are available in the system, * fetched either from the TimeZone class or from our memory cache. * * @return The array. */ public static String[] getKnownTimezones() { // TODO Drop the Linux ZoneInfo stuff in favor of ICU. if (availableTimezones == null) { availableTimezones = TimeZone.getAvailableIDs(); } return availableTimezones.clone(); } /** * Returns the display name for the given time zone using the given locale. * * @param id The time zone ID, for example "Europe/Berlin" * @param daylight Indicates whether daylight savings is in use * @param style The style, 0 for long, 1 for short * @param locale The locale name, for example "en_US". * @return The desired display name */ public static String getDisplayTimeZone(String id, boolean daylight, int style, String locale) { // If we already have the strings, linear search through them is 10x quicker than // calling ICU for just the one we want. if (DefaultTimeZones.locale.equals(locale)) { String result = lookupDisplayTimeZone(DefaultTimeZones.names, id, daylight, style); if (result != null) { return result; } } return getDisplayTimeZoneNative(id, daylight, style, locale); } public static String lookupDisplayTimeZone(String[][] zoneStrings, String id, boolean daylight, int style) { for (String[] row : zoneStrings) { if (row[0].equals(id)) { if (daylight) { return (style == TimeZone.LONG) ? row[3] : row[4]; } else { return (style == TimeZone.LONG) ? row[1] : row[2]; } } } return null; } /** * Initialization holder for default time zone names. This class will * be preloaded by the zygote to share the time and space costs of setting * up the list of time zone names, so although it looks like the lazy * initialization idiom, it's actually the opposite. */ private static class DefaultTimeZones { /** * Name of default locale at the time this class was initialized. */ private static final String locale = Locale.getDefault().toString(); /** * Names of time zones for the default locale. */ private static final String[][] names = createTimeZoneNamesFor(locale); } /** * Creates array of time zone names for the given locale. This method takes * about 2s to run on a 400MHz ARM11. */ private static String[][] createTimeZoneNamesFor(String locale) { long start = System.currentTimeMillis(); /* * The following code is optimized for fast native response (the time a * method call can be in native code is limited). It prepares an empty * array to keep native code from having to create new Java objects. It * also fill in the time zone IDs to speed things up a bit. There's one * array for each time zone name type. (standard/long, standard/short, * daylight/long, daylight/short) The native method that fetches these * strings is faster if it can do all entries of one type, before having * to change to the next type. That's why the array passed down to * native has 5 entries, each providing space for all time zone names of * one type. Likely this access to the fields is much faster in the * native code because there's less array access overhead. */ String[][] arrayToFill = new String[5][]; arrayToFill[0] = getKnownTimezones(); arrayToFill[1] = new String[availableTimezones.length]; arrayToFill[2] = new String[availableTimezones.length]; arrayToFill[3] = new String[availableTimezones.length]; arrayToFill[4] = new String[availableTimezones.length]; /* * Fill in the zone names in native. */ getTimeZonesNative(arrayToFill, locale); /* * Finally we need to reorder the entries so we get the expected result. */ String[][] result = new String[availableTimezones.length][5]; for (int i = 0; i < availableTimezones.length; i++) { result[i][0] = arrayToFill[0][i]; result[i][1] = arrayToFill[1][i]; result[i][2] = arrayToFill[2][i]; result[i][3] = arrayToFill[3][i]; result[i][4] = arrayToFill[4][i]; } Logger.getLogger(Resources.class.getSimpleName()).info( "Loaded time zone names for " + locale + " in " + (System.currentTimeMillis() - start) + "ms."); return result; } /** * Returns the display names for all given timezones using the given locale. * * @return An array of time zone strings. Each row represents one time zone. * The first columns holds the ID of the time zone, for example * "Europe/Berlin". The other columns then hold for each row the * four time zone names with and without daylight savings and in * long and short format. It's exactly the array layout required by * the TimeZone class. */ public static String[][] getDisplayTimeZones(String locale) { String defaultLocale = Locale.getDefault().toString(); if (locale == null) { locale = defaultLocale; } // If locale == default and the default locale hasn't changed since // DefaultTimeZones loaded, return the cached names. // TODO: We should force a reboot if the default locale changes. if (defaultLocale.equals(locale) && DefaultTimeZones.locale.equals(defaultLocale)) { return clone2dStringArray(DefaultTimeZones.names); } return createTimeZoneNamesFor(locale); } public static String[][] clone2dStringArray(String[][] array) { String[][] result = new String[array.length][]; for (int i = 0; i < array.length; ++i) { result[i] = array[i].clone(); } return result; } // --- Native methods accessing ICU's database ---------------------------- public static native String getDisplayCountryNative(String countryCode, String locale); public static native String getDisplayLanguageNative(String languageCode, String locale); public static native String getDisplayVariantNative(String variantCode, String locale); public static native String getISO3CountryNative(String locale); public static native String getISO3LanguageNative(String locale); public static native String getCurrencyCodeNative(String locale); public static native String getCurrencySymbolNative(String locale, String currencyCode); public static native int getCurrencyFractionDigitsNative(String currencyCode); private static native String[] getAvailableLocalesNative(); private static native String[] getISOLanguagesNative(); private static native String[] getISOCountriesNative(); private static native void getTimeZonesNative(String[][] arrayToFill, String locale); private static native String getDisplayTimeZoneNative(String id, boolean isDST, int style, String locale); private static LocaleData initLocaleData(Locale locale) { LocaleData localeData = new LocaleData(); if (!initLocaleDataImpl(locale.toString(), localeData)) { throw new AssertionError("couldn't initialize LocaleData for locale " + locale); } if (localeData.fullTimeFormat != null) { // There are some full time format patterns in ICU that use the pattern character 'v'. // Java doesn't accept this, so we replace it with 'z' which has about the same result // as 'v', the timezone name. // 'v' -> "PT", 'z' -> "PST", v is the generic timezone and z the standard tz // "vvvv" -> "Pacific Time", "zzzz" -> "Pacific Standard Time" localeData.fullTimeFormat = localeData.fullTimeFormat.replace('v', 'z'); } if (localeData.numberPattern != null) { // The number pattern might contain positive and negative subpatterns. Arabic, for // example, might look like "#,##0.###;#,##0.###-" because the minus sign should be // written last. Macedonian supposedly looks something like "#,##0.###;(#,##0.###)". // (The negative subpattern is optional, though, and not present in most locales.) // By only swallowing '#'es and ','s after the '.', we ensure that we don't // accidentally eat too much. localeData.integerPattern = localeData.numberPattern.replaceAll("\\.[#,]*", ""); } return localeData; } private static native boolean initLocaleDataImpl(String locale, LocaleData result); }