// © 2016 and later: Unicode, Inc. and others. // License & terms of use: http://www.unicode.org/copyright.html#License /* * ***************************************************************************** * Copyright (C) 2005-2016, International Business Machines Corporation and * others. All Rights Reserved. * ***************************************************************************** */ package com.ibm.icu.impl; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Locale; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.Set; import com.ibm.icu.impl.ICUResourceBundleReader.ReaderValue; import com.ibm.icu.impl.URLHandler.URLVisitor; import com.ibm.icu.util.ULocale; import com.ibm.icu.util.UResourceBundle; import com.ibm.icu.util.UResourceBundleIterator; import com.ibm.icu.util.UResourceTypeMismatchException; public class ICUResourceBundle extends UResourceBundle { /** * CLDR string value "∅∅∅" prevents fallback to the parent bundle. */ public static final String NO_INHERITANCE_MARKER = "\u2205\u2205\u2205"; /** * The class loader constant to be used with getBundleInstance API */ public static final ClassLoader ICU_DATA_CLASS_LOADER = ClassLoaderUtil.getClassLoader(ICUData.class); /** * The name of the resource containing the installed locales */ protected static final String INSTALLED_LOCALES = "InstalledLocales"; /** * Fields for a whole bundle, rather than any specific resource in the bundle. * Corresponds roughly to ICU4C/source/common/uresimp.h struct UResourceDataEntry. */ protected static final class WholeBundle { WholeBundle(String baseName, String localeID, ClassLoader loader, ICUResourceBundleReader reader) { this.baseName = baseName; this.localeID = localeID; this.ulocale = new ULocale(localeID); this.loader = loader; this.reader = reader; } String baseName; String localeID; ULocale ulocale; ClassLoader loader; /** * Access to the bits and bytes of the resource bundle. * Hides low-level details. */ ICUResourceBundleReader reader; // TODO: Remove topLevelKeys when we upgrade to Java 6 where ResourceBundle caches the keySet(). Set<String> topLevelKeys; } WholeBundle wholeBundle; private ICUResourceBundle container; /** Loader for bundle instances, for caching. */ private static abstract class Loader { abstract ICUResourceBundle load(); } private static CacheBase<String, ICUResourceBundle, Loader> BUNDLE_CACHE = new SoftCache<String, ICUResourceBundle, Loader>() { @Override protected ICUResourceBundle createInstance(String unusedKey, Loader loader) { return loader.load(); } }; /** * Returns a functionally equivalent locale, considering keywords as well, for the specified keyword. * @param baseName resource specifier * @param resName top level resource to consider (such as "collations") * @param keyword a particular keyword to consider (such as "collation" ) * @param locID The requested locale * @param isAvailable If non-null, 1-element array of fillin parameter that indicates whether the * requested locale was available. The locale is defined as 'available' if it physically * exists within the specified tree and included in 'InstalledLocales'. * @param omitDefault if true, omit keyword and value if default. * 'de_DE\@collation=standard' -> 'de_DE' * @return the locale * @internal ICU 3.0 */ public static final ULocale getFunctionalEquivalent(String baseName, ClassLoader loader, String resName, String keyword, ULocale locID, boolean isAvailable[], boolean omitDefault) { String kwVal = locID.getKeywordValue(keyword); String baseLoc = locID.getBaseName(); String defStr = null; ULocale parent = new ULocale(baseLoc); ULocale defLoc = null; // locale where default (found) resource is boolean lookForDefault = false; // true if kwVal needs to be set ULocale fullBase = null; // base locale of found (target) resource int defDepth = 0; // depth of 'default' marker int resDepth = 0; // depth of found resource; if ((kwVal == null) || (kwVal.length() == 0) || kwVal.equals(DEFAULT_TAG)) { kwVal = ""; // default tag is treated as no keyword lookForDefault = true; } // Check top level locale first ICUResourceBundle r = null; r = (ICUResourceBundle) UResourceBundle.getBundleInstance(baseName, parent); if (isAvailable != null) { isAvailable[0] = false; ULocale[] availableULocales = getAvailEntry(baseName, loader).getULocaleList(); for (int i = 0; i < availableULocales.length; i++) { if (parent.equals(availableULocales[i])) { isAvailable[0] = true; break; } } } // determine in which locale (if any) the currently relevant 'default' is do { try { ICUResourceBundle irb = (ICUResourceBundle) r.get(resName); defStr = irb.getString(DEFAULT_TAG); if (lookForDefault == true) { kwVal = defStr; lookForDefault = false; } defLoc = r.getULocale(); } catch (MissingResourceException t) { // Ignore error and continue search. } if (defLoc == null) { r = r.getParent(); defDepth++; } } while ((r != null) && (defLoc == null)); // Now, search for the named resource parent = new ULocale(baseLoc); r = (ICUResourceBundle) UResourceBundle.getBundleInstance(baseName, parent); // determine in which locale (if any) the named resource is located do { try { ICUResourceBundle irb = (ICUResourceBundle)r.get(resName); /* UResourceBundle urb = */irb.get(kwVal); fullBase = irb.getULocale(); // If the get() completed, we have the full base locale // If we fell back to an ancestor of the old 'default', // we need to re calculate the "default" keyword. if ((fullBase != null) && ((resDepth) > defDepth)) { defStr = irb.getString(DEFAULT_TAG); defLoc = r.getULocale(); defDepth = resDepth; } } catch (MissingResourceException t) { // Ignore error, } if (fullBase == null) { r = r.getParent(); resDepth++; } } while ((r != null) && (fullBase == null)); if (fullBase == null && // Could not find resource 'kwVal' (defStr != null) && // default was defined !defStr.equals(kwVal)) { // kwVal is not default // couldn't find requested resource. Fall back to default. kwVal = defStr; // Fall back to default. parent = new ULocale(baseLoc); r = (ICUResourceBundle) UResourceBundle.getBundleInstance(baseName, parent); resDepth = 0; // determine in which locale (if any) the named resource is located do { try { ICUResourceBundle irb = (ICUResourceBundle)r.get(resName); ICUResourceBundle urb = (ICUResourceBundle)irb.get(kwVal); // if we didn't fail before this.. fullBase = r.getULocale(); // If the fetched item (urb) is in a different locale than our outer locale (r/fullBase) // then we are in a 'fallback' situation. treat as a missing resource situation. if(!fullBase.getBaseName().equals(urb.getULocale().getBaseName())) { fullBase = null; // fallback condition. Loop and try again. } // If we fell back to an ancestor of the old 'default', // we need to re calculate the "default" keyword. if ((fullBase != null) && ((resDepth) > defDepth)) { defStr = irb.getString(DEFAULT_TAG); defLoc = r.getULocale(); defDepth = resDepth; } } catch (MissingResourceException t) { // Ignore error, continue search. } if (fullBase == null) { r = r.getParent(); resDepth++; } } while ((r != null) && (fullBase == null)); } if (fullBase == null) { throw new MissingResourceException( "Could not find locale containing requested or default keyword.", baseName, keyword + "=" + kwVal); } if (omitDefault && defStr.equals(kwVal) // if default was requested and && resDepth <= defDepth) { // default was set in same locale or child return fullBase; // Keyword value is default - no keyword needed in locale } else { return new ULocale(fullBase.getBaseName() + "@" + keyword + "=" + kwVal); } } /** * Given a tree path and keyword, return a string enumeration of all possible values for that keyword. * @param baseName resource specifier * @param keyword a particular keyword to consider, must match a top level resource name * within the tree. (i.e. "collations") * @internal ICU 3.0 */ public static final String[] getKeywordValues(String baseName, String keyword) { Set<String> keywords = new HashSet<String>(); ULocale locales[] = getAvailEntry(baseName, ICU_DATA_CLASS_LOADER).getULocaleList(); int i; for (i = 0; i < locales.length; i++) { try { UResourceBundle b = UResourceBundle.getBundleInstance(baseName, locales[i]); // downcast to ICUResourceBundle? ICUResourceBundle irb = (ICUResourceBundle) (b.getObject(keyword)); Enumeration<String> e = irb.getKeys(); while (e.hasMoreElements()) { String s = e.nextElement(); if (!DEFAULT_TAG.equals(s) && !s.startsWith("private-")) { // don't add 'default' items, nor unlisted types keywords.add(s); } } } catch (Throwable t) { //System.err.println("Error in - " + new Integer(i).toString() // + " - " + t.toString()); // ignore the err - just skip that resource } } return keywords.toArray(new String[0]); } /** * This method performs multilevel fallback for fetching items from the * bundle e.g: If resource is in the form de__PHONEBOOK{ collations{ * default{ "phonebook"} } } If the value of "default" key needs to be * accessed, then do: <code> * UResourceBundle bundle = UResourceBundle.getBundleInstance("de__PHONEBOOK"); * ICUResourceBundle result = null; * if(bundle instanceof ICUResourceBundle){ * result = ((ICUResourceBundle) bundle).getWithFallback("collations/default"); * } * </code> * * @param path The path to the required resource key * @return resource represented by the key * @exception MissingResourceException If a resource was not found. */ public ICUResourceBundle getWithFallback(String path) throws MissingResourceException { ICUResourceBundle actualBundle = this; // now recurse to pick up sub levels of the items ICUResourceBundle result = findResourceWithFallback(path, actualBundle, null); if (result == null) { throw new MissingResourceException( "Can't find resource for bundle " + this.getClass().getName() + ", key " + getType(), path, getKey()); } if (result.getType() == STRING && result.getString().equals(NO_INHERITANCE_MARKER)) { throw new MissingResourceException("Encountered NO_INHERITANCE_MARKER", path, getKey()); } return result; } public ICUResourceBundle at(int index) { return (ICUResourceBundle) handleGet(index, null, this); } public ICUResourceBundle at(String key) { // don't ever presume the key is an int in disguise, like ResourceArray does. if (this instanceof ICUResourceBundleImpl.ResourceTable) { return (ICUResourceBundle) handleGet(key, null, this); } return null; } @Override public ICUResourceBundle findTopLevel(int index) { return (ICUResourceBundle) super.findTopLevel(index); } @Override public ICUResourceBundle findTopLevel(String aKey) { return (ICUResourceBundle) super.findTopLevel(aKey); } /** * Like getWithFallback, but returns null if the resource is not found instead of * throwing an exception. * @param path the path to the resource * @return the resource, or null */ public ICUResourceBundle findWithFallback(String path) { return findResourceWithFallback(path, this, null); } public String findStringWithFallback(String path) { return findStringWithFallback(path, this, null); } // will throw type mismatch exception if the resource is not a string public String getStringWithFallback(String path) throws MissingResourceException { // Optimized form of getWithFallback(path).getString(); ICUResourceBundle actualBundle = this; String result = findStringWithFallback(path, actualBundle, null); if (result == null) { throw new MissingResourceException( "Can't find resource for bundle " + this.getClass().getName() + ", key " + getType(), path, getKey()); } if (result.equals(NO_INHERITANCE_MARKER)) { throw new MissingResourceException("Encountered NO_INHERITANCE_MARKER", path, getKey()); } return result; } public void getAllItemsWithFallback(String path, UResource.Sink sink) throws MissingResourceException { // Collect existing and parsed key objects into an array of keys, // rather than assembling and parsing paths. int numPathKeys = countPathKeys(path); // How much deeper does the path go? ICUResourceBundle rb; if (numPathKeys == 0) { rb = this; } else { // Get the keys for finding the target. int depth = getResDepth(); // How deep are we in this bundle? String[] pathKeys = new String[depth + numPathKeys]; getResPathKeys(path, numPathKeys, pathKeys, depth); rb = findResourceWithFallback(pathKeys, depth, this, null); if (rb == null) { throw new MissingResourceException( "Can't find resource for bundle " + this.getClass().getName() + ", key " + getType(), path, getKey()); } } UResource.Key key = new UResource.Key(); ReaderValue readerValue = new ReaderValue(); rb.getAllItemsWithFallback(key, readerValue, sink); } private void getAllItemsWithFallback( UResource.Key key, ReaderValue readerValue, UResource.Sink sink) { // We recursively enumerate child-first, // only storing parent items in the absence of child items. // The sink needs to store a placeholder value for the no-fallback/no-inheritance marker // to prevent a parent item from being stored. // // It would be possible to recursively enumerate parent-first, // overriding parent items with child items. // When the sink sees the no-fallback/no-inheritance marker, // then it would remove the parent's item. // We would deserialize parent values even though they are overridden in a child bundle. ICUResourceBundleImpl impl = (ICUResourceBundleImpl)this; readerValue.reader = impl.wholeBundle.reader; readerValue.res = impl.getResource(); key.setString(this.key != null ? this.key : ""); sink.put(key, readerValue, parent == null); if (parent != null) { // We might try to query the sink whether // any fallback from the parent bundle is still possible. ICUResourceBundle parentBundle = (ICUResourceBundle)parent; ICUResourceBundle rb; int depth = getResDepth(); if (depth == 0) { rb = parentBundle; } else { // Re-fetch the path keys: They may differ from the original ones // if we had followed an alias. String[] pathKeys = new String[depth]; getResPathKeys(pathKeys, depth); rb = findResourceWithFallback(pathKeys, 0, parentBundle, null); } if (rb != null) { rb.getAllItemsWithFallback(key, readerValue, sink); } } } /** * Return a set of the locale names supported by a collection of resource * bundles. * * @param bundlePrefix the prefix of the resource bundles to use. */ public static Set<String> getAvailableLocaleNameSet(String bundlePrefix, ClassLoader loader) { return getAvailEntry(bundlePrefix, loader).getLocaleNameSet(); } /** * Return a set of all the locale names supported by a collection of * resource bundles. */ public static Set<String> getFullLocaleNameSet() { return getFullLocaleNameSet(ICUData.ICU_BASE_NAME, ICU_DATA_CLASS_LOADER); } /** * Return a set of all the locale names supported by a collection of * resource bundles. * * @param bundlePrefix the prefix of the resource bundles to use. */ public static Set<String> getFullLocaleNameSet(String bundlePrefix, ClassLoader loader) { return getAvailEntry(bundlePrefix, loader).getFullLocaleNameSet(); } /** * Return a set of the locale names supported by a collection of resource * bundles. */ public static Set<String> getAvailableLocaleNameSet() { return getAvailableLocaleNameSet(ICUData.ICU_BASE_NAME, ICU_DATA_CLASS_LOADER); } /** * Get the set of Locales installed in the specified bundles. * @return the list of available locales */ public static final ULocale[] getAvailableULocales(String baseName, ClassLoader loader) { return getAvailEntry(baseName, loader).getULocaleList(); } /** * Get the set of ULocales installed the base bundle. * @return the list of available locales */ public static final ULocale[] getAvailableULocales() { return getAvailableULocales(ICUData.ICU_BASE_NAME, ICU_DATA_CLASS_LOADER); } /** * Get the set of Locales installed in the specified bundles. * @return the list of available locales */ public static final Locale[] getAvailableLocales(String baseName, ClassLoader loader) { return getAvailEntry(baseName, loader).getLocaleList(); } /** * Get the set of Locales installed the base bundle. * @return the list of available locales */ public static final Locale[] getAvailableLocales() { return getAvailEntry(ICUData.ICU_BASE_NAME, ICU_DATA_CLASS_LOADER).getLocaleList(); } /** * Convert a list of ULocales to a list of Locales. ULocales with a script code will not be converted * since they cannot be represented as a Locale. This means that the two lists will <b>not</b> match * one-to-one, and that the returned list might be shorter than the input list. * @param ulocales a list of ULocales to convert to a list of Locales. * @return the list of converted ULocales */ public static final Locale[] getLocaleList(ULocale[] ulocales) { ArrayList<Locale> list = new ArrayList<Locale>(ulocales.length); HashSet<Locale> uniqueSet = new HashSet<Locale>(); for (int i = 0; i < ulocales.length; i++) { Locale loc = ulocales[i].toLocale(); if (!uniqueSet.contains(loc)) { list.add(loc); uniqueSet.add(loc); } } return list.toArray(new Locale[list.size()]); } /** * Returns the locale of this resource bundle. This method can be used after * a call to getBundle() to determine whether the resource bundle returned * really corresponds to the requested locale or is a fallback. * * @return the locale of this resource bundle */ @Override public Locale getLocale() { return getULocale().toLocale(); } // ========== privates ========== private static final String ICU_RESOURCE_INDEX = "res_index"; private static final String DEFAULT_TAG = "default"; // The name of text file generated by ICU4J build script including all locale names // (canonical, alias and root) private static final String FULL_LOCALE_NAMES_LIST = "fullLocaleNames.lst"; // Flag for enabling/disabling debugging code private static final boolean DEBUG = ICUDebug.enabled("localedata"); private static final ULocale[] createULocaleList(String baseName, ClassLoader root) { // the canned list is a subset of all the available .res files, the idea // is we don't export them // all. gotta be a better way to do this, since to add a locale you have // to update this list, // and it's embedded in our binary resources. ICUResourceBundle bundle = (ICUResourceBundle) UResourceBundle.instantiateBundle(baseName, ICU_RESOURCE_INDEX, root, true); bundle = (ICUResourceBundle)bundle.get(INSTALLED_LOCALES); int length = bundle.getSize(); int i = 0; ULocale[] locales = new ULocale[length]; UResourceBundleIterator iter = bundle.getIterator(); iter.reset(); while (iter.hasNext()) { String locstr = iter.next().getKey(); if (locstr.equals("root")) { locales[i++] = ULocale.ROOT; } else { locales[i++] = new ULocale(locstr); } } bundle = null; return locales; } // Same as createULocaleList() but catches the MissingResourceException // and returns the data in a different form. private static final void addLocaleIDsFromIndexBundle(String baseName, ClassLoader root, Set<String> locales) { ICUResourceBundle bundle; try { bundle = (ICUResourceBundle) UResourceBundle.instantiateBundle(baseName, ICU_RESOURCE_INDEX, root, true); bundle = (ICUResourceBundle) bundle.get(INSTALLED_LOCALES); } catch (MissingResourceException e) { if (DEBUG) { System.out.println("couldn't find " + baseName + '/' + ICU_RESOURCE_INDEX + ".res"); Thread.dumpStack(); } return; } UResourceBundleIterator iter = bundle.getIterator(); iter.reset(); while (iter.hasNext()) { String locstr = iter.next(). getKey(); locales.add(locstr); } } private static final void addBundleBaseNamesFromClassLoader( final String bn, final ClassLoader root, final Set<String> names) { java.security.AccessController .doPrivileged(new java.security.PrivilegedAction<Void>() { @Override public Void run() { try { // bn has a trailing slash: The WebSphere class loader would return null // for a raw directory name without it. Enumeration<URL> urls = root.getResources(bn); if (urls == null) { return null; } URLVisitor v = new URLVisitor() { @Override public void visit(String s) { if (s.endsWith(".res")) { String locstr = s.substring(0, s.length() - 4); names.add(locstr); } } }; while (urls.hasMoreElements()) { URL url = urls.nextElement(); URLHandler handler = URLHandler.get(url); if (handler != null) { handler.guide(v, false); } else { if (DEBUG) System.out.println("handler for " + url + " is null"); } } } catch (IOException e) { if (DEBUG) System.out.println("ouch: " + e.getMessage()); } return null; } }); } private static void addLocaleIDsFromListFile(String bn, ClassLoader root, Set<String> locales) { try { InputStream s = root.getResourceAsStream(bn + FULL_LOCALE_NAMES_LIST); if (s != null) { BufferedReader br = new BufferedReader(new InputStreamReader(s, "ASCII")); try { String line; while ((line = br.readLine()) != null) { if (line.length() != 0 && !line.startsWith("#")) { locales.add(line); } } } finally { br.close(); } } } catch (IOException ignored) { // swallow it } } private static Set<String> createFullLocaleNameSet(String baseName, ClassLoader loader) { String bn = baseName.endsWith("/") ? baseName : baseName + "/"; Set<String> set = new HashSet<String>(); String skipScan = ICUConfig.get("com.ibm.icu.impl.ICUResourceBundle.skipRuntimeLocaleResourceScan", "false"); if (!skipScan.equalsIgnoreCase("true")) { // scan available locale resources under the base url first addBundleBaseNamesFromClassLoader(bn, loader, set); if (baseName.startsWith(ICUData.ICU_BASE_NAME)) { String folder; if (baseName.length() == ICUData.ICU_BASE_NAME.length()) { folder = ""; } else if (baseName.charAt(ICUData.ICU_BASE_NAME.length()) == '/') { folder = baseName.substring(ICUData.ICU_BASE_NAME.length() + 1); } else { folder = null; } if (folder != null) { ICUBinary.addBaseNamesInFileFolder(folder, ".res", set); } } set.remove(ICU_RESOURCE_INDEX); // "res_index" // HACK: TODO: Figure out how we can distinguish locale data from other data items. Iterator<String> iter = set.iterator(); while (iter.hasNext()) { String name = iter.next(); if ((name.length() == 1 || name.length() > 3) && name.indexOf('_') < 0) { // Does not look like a locale ID. iter.remove(); } } } // look for prebuilt full locale names list next if (set.isEmpty()) { if (DEBUG) System.out.println("unable to enumerate data files in " + baseName); addLocaleIDsFromListFile(bn, loader, set); } if (set.isEmpty()) { // Use locale name set as the last resort fallback addLocaleIDsFromIndexBundle(baseName, loader, set); } // We need to have the root locale in the set, but not as "root". set.remove("root"); set.add(ULocale.ROOT.toString()); // "" return Collections.unmodifiableSet(set); } private static Set<String> createLocaleNameSet(String baseName, ClassLoader loader) { HashSet<String> set = new HashSet<String>(); addLocaleIDsFromIndexBundle(baseName, loader, set); return Collections.unmodifiableSet(set); } /** * Holds the prefix, and lazily creates the Locale[] list or the locale name * Set as needed. */ private static final class AvailEntry { private String prefix; private ClassLoader loader; private volatile ULocale[] ulocales; private volatile Locale[] locales; private volatile Set<String> nameSet; private volatile Set<String> fullNameSet; AvailEntry(String prefix, ClassLoader loader) { this.prefix = prefix; this.loader = loader; } ULocale[] getULocaleList() { if (ulocales == null) { synchronized(this) { if (ulocales == null) { ulocales = createULocaleList(prefix, loader); } } } return ulocales; } Locale[] getLocaleList() { if (locales == null) { getULocaleList(); synchronized(this) { if (locales == null) { locales = ICUResourceBundle.getLocaleList(ulocales); } } } return locales; } Set<String> getLocaleNameSet() { if (nameSet == null) { synchronized(this) { if (nameSet == null) { nameSet = createLocaleNameSet(prefix, loader); } } } return nameSet; } Set<String> getFullLocaleNameSet() { // When there's no prebuilt index, we iterate through the jar files // and read the contents to build it. If many threads try to read the // same jar at the same time, java thrashes. Synchronize here // so that we can avoid this problem. We don't synchronize on the // other methods since they don't do this. // // This is the common entry point for access into the code that walks // through the resources, and is cached. So it's a good place to lock // access. Locking in the URLHandler doesn't give us a common object // to lock. if (fullNameSet == null) { synchronized(this) { if (fullNameSet == null) { fullNameSet = createFullLocaleNameSet(prefix, loader); } } } return fullNameSet; } } /* * Cache used for AvailableEntry */ private static CacheBase<String, AvailEntry, ClassLoader> GET_AVAILABLE_CACHE = new SoftCache<String, AvailEntry, ClassLoader>() { @Override protected AvailEntry createInstance(String key, ClassLoader loader) { return new AvailEntry(key, loader); } }; /** * Stores the locale information in a cache accessed by key (bundle prefix). * The cached objects are AvailEntries. The cache is implemented by SoftCache * so it can be GC'd. */ private static AvailEntry getAvailEntry(String key, ClassLoader loader) { return GET_AVAILABLE_CACHE.getInstance(key, loader); } private static final ICUResourceBundle findResourceWithFallback(String path, UResourceBundle actualBundle, UResourceBundle requested) { if (path.length() == 0) { return null; } ICUResourceBundle base = (ICUResourceBundle) actualBundle; // Collect existing and parsed key objects into an array of keys, // rather than assembling and parsing paths. int depth = base.getResDepth(); int numPathKeys = countPathKeys(path); assert numPathKeys > 0; String[] keys = new String[depth + numPathKeys]; getResPathKeys(path, numPathKeys, keys, depth); return findResourceWithFallback(keys, depth, base, requested); } private static final ICUResourceBundle findResourceWithFallback( String[] keys, int depth, ICUResourceBundle base, UResourceBundle requested) { if (requested == null) { requested = base; } for (;;) { // Iterate over the parent bundles. for (;;) { // Iterate over the keys on the requested path, within a bundle. String subKey = keys[depth++]; ICUResourceBundle sub = (ICUResourceBundle) base.handleGet(subKey, null, requested); if (sub == null) { --depth; break; } if (depth == keys.length) { // We found it. return sub; } base = sub; } // Try the parent bundle of the last-found resource. ICUResourceBundle nextBase = base.getParent(); if (nextBase == null) { return null; } // If we followed an alias, then we may have switched bundle (locale) and key path. // Set the lower parts of the path according to the last-found resource. // This relies on a resource found via alias to have its original location information, // rather than the location of the alias. int baseDepth = base.getResDepth(); if (depth != baseDepth) { String[] newKeys = new String[baseDepth + (keys.length - depth)]; System.arraycopy(keys, depth, newKeys, baseDepth, keys.length - depth); keys = newKeys; } base.getResPathKeys(keys, baseDepth); base = nextBase; depth = 0; // getParent() returned a top level table resource. } } /** * Like findResourceWithFallback(...).getString() but with minimal creation of intermediate * ICUResourceBundle objects. */ private static final String findStringWithFallback(String path, UResourceBundle actualBundle, UResourceBundle requested) { if (path.length() == 0) { return null; } if (!(actualBundle instanceof ICUResourceBundleImpl.ResourceContainer)) { return null; } if (requested == null) { requested = actualBundle; } ICUResourceBundle base = (ICUResourceBundle) actualBundle; ICUResourceBundleReader reader = base.wholeBundle.reader; int res = RES_BOGUS; // Collect existing and parsed key objects into an array of keys, // rather than assembling and parsing paths. int baseDepth = base.getResDepth(); int depth = baseDepth; int numPathKeys = countPathKeys(path); assert numPathKeys > 0; String[] keys = new String[depth + numPathKeys]; getResPathKeys(path, numPathKeys, keys, depth); for (;;) { // Iterate over the parent bundles. for (;;) { // Iterate over the keys on the requested path, within a bundle. ICUResourceBundleReader.Container readerContainer; if (res == RES_BOGUS) { int type = base.getType(); if (type == TABLE || type == ARRAY) { readerContainer = ((ICUResourceBundleImpl.ResourceContainer)base).value; } else { break; } } else { int type = ICUResourceBundleReader.RES_GET_TYPE(res); if (ICUResourceBundleReader.URES_IS_TABLE(type)) { readerContainer = reader.getTable(res); } else if (ICUResourceBundleReader.URES_IS_ARRAY(type)) { readerContainer = reader.getArray(res); } else { res = RES_BOGUS; break; } } String subKey = keys[depth++]; res = readerContainer.getResource(reader, subKey); if (res == RES_BOGUS) { --depth; break; } ICUResourceBundle sub; if (ICUResourceBundleReader.RES_GET_TYPE(res) == ALIAS) { base.getResPathKeys(keys, baseDepth); sub = getAliasedResource(base, keys, depth, subKey, res, null, requested); } else { sub = null; } if (depth == keys.length) { // We found it. if (sub != null) { return sub.getString(); // string from alias handling } else { String s = reader.getString(res); if (s == null) { throw new UResourceTypeMismatchException(""); } return s; } } if (sub != null) { base = sub; reader = base.wholeBundle.reader; res = RES_BOGUS; // If we followed an alias, then we may have switched bundle (locale) and key path. // Reserve space for the lower parts of the path according to the last-found resource. // This relies on a resource found via alias to have its original location information, // rather than the location of the alias. baseDepth = base.getResDepth(); if (depth != baseDepth) { String[] newKeys = new String[baseDepth + (keys.length - depth)]; System.arraycopy(keys, depth, newKeys, baseDepth, keys.length - depth); keys = newKeys; depth = baseDepth; } } } // Try the parent bundle of the last-found resource. ICUResourceBundle nextBase = base.getParent(); if (nextBase == null) { return null; } // We probably have not yet set the lower parts of the key path. base.getResPathKeys(keys, baseDepth); base = nextBase; reader = base.wholeBundle.reader; depth = baseDepth = 0; // getParent() returned a top level table resource. } } private int getResDepth() { return (container == null) ? 0 : container.getResDepth() + 1; } /** * Fills some of the keys array with the keys on the path to this resource object. * Writes the top-level key into index 0 and increments from there. * * @param keys * @param depth must be {@link #getResDepth()} */ private void getResPathKeys(String[] keys, int depth) { ICUResourceBundle b = this; while (depth > 0) { keys[--depth] = b.key; b = b.container; assert (depth == 0) == (b.container == null); } } private static int countPathKeys(String path) { if (path.isEmpty()) { return 0; } int num = 1; for (int i = 0; i < path.length(); ++i) { if (path.charAt(i) == RES_PATH_SEP_CHAR) { ++num; } } return num; } /** * Fills some of the keys array (from start) with the num keys from the path string. * * @param path path string * @param num must be {@link #countPathKeys(String)} * @param keys * @param start index where the first path key is stored */ private static void getResPathKeys(String path, int num, String[] keys, int start) { if (num == 0) { return; } if (num == 1) { keys[start] = path; return; } int i = 0; for (;;) { int j = path.indexOf(RES_PATH_SEP_CHAR, i); assert j >= i; keys[start++] = path.substring(i, j); if (num == 2) { assert path.indexOf(RES_PATH_SEP_CHAR, j + 1) < 0; keys[start] = path.substring(j + 1); break; } else { i = j + 1; --num; } } } @Override public boolean equals(Object other) { if (this == other) { return true; } if (other instanceof ICUResourceBundle) { ICUResourceBundle o = (ICUResourceBundle) other; if (getBaseName().equals(o.getBaseName()) && getLocaleID().equals(o.getLocaleID())) { return true; } } return false; } @Override public int hashCode() { assert false : "hashCode not designed"; return 42; } public enum OpenType { // C++ uresbund.cpp: enum UResOpenType /** * Open a resource bundle for the locale; * if there is not even a base language bundle, then fall back to the default locale; * if there is no bundle for that either, then load the root bundle. * * <p>This is the default bundle loading behavior. */ LOCALE_DEFAULT_ROOT, // TODO: ICU ticket #11271 "consistent default locale across locale trees" // Add an option to look at the main locale tree for whether to // fall back to root directly (if the locale has main data) or // fall back to the default locale first (if the locale does not even have main data). /** * Open a resource bundle for the locale; * if there is not even a base language bundle, then load the root bundle; * never fall back to the default locale. * * <p>This is used for algorithms that have good pan-Unicode default behavior, * such as case mappings, collation, and segmentation (BreakIterator). */ LOCALE_ROOT, /** * Open a resource bundle for the locale; * if there is not even a base language bundle, then fail; * never fall back to the default locale nor to the root locale. * * <p>This is used when fallback to another language is not desired * and the root locale is not generally useful. * For example, {@link com.ibm.icu.util.LocaleData#setNoSubstitute(boolean)} * or currency display names for {@link com.ibm.icu.text.LocaleDisplayNames}. */ LOCALE_ONLY, /** * Open a resource bundle for the exact bundle name as requested; * no fallbacks, do not load parent bundles. * * <p>This is used for supplemental (non-locale) data. */ DIRECT }; // This method is for super class's instantiateBundle method public static ICUResourceBundle getBundleInstance(String baseName, String localeID, ClassLoader root, boolean disableFallback) { return getBundleInstance(baseName, localeID, root, disableFallback ? OpenType.DIRECT : OpenType.LOCALE_DEFAULT_ROOT); } public static ICUResourceBundle getBundleInstance( String baseName, ULocale locale, OpenType openType) { if (locale == null) { locale = ULocale.getDefault(); } return getBundleInstance(baseName, locale.getBaseName(), ICUResourceBundle.ICU_DATA_CLASS_LOADER, openType); } public static ICUResourceBundle getBundleInstance(String baseName, String localeID, ClassLoader root, OpenType openType) { if (baseName == null) { baseName = ICUData.ICU_BASE_NAME; } localeID = ULocale.getBaseName(localeID); ICUResourceBundle b; if (openType == OpenType.LOCALE_DEFAULT_ROOT) { b = instantiateBundle(baseName, localeID, ULocale.getDefault().getBaseName(), root, openType); } else { b = instantiateBundle(baseName, localeID, null, root, openType); } if(b==null){ throw new MissingResourceException( "Could not find the bundle "+ baseName+"/"+ localeID+".res","",""); } return b; } private static boolean localeIDStartsWithLangSubtag(String localeID, String lang) { return localeID.startsWith(lang) && (localeID.length() == lang.length() || localeID.charAt(lang.length()) == '_'); } private static ICUResourceBundle instantiateBundle( final String baseName, final String localeID, final String defaultID, final ClassLoader root, final OpenType openType) { assert localeID.indexOf('@') < 0; assert defaultID == null || defaultID.indexOf('@') < 0; final String fullName = ICUResourceBundleReader.getFullName(baseName, localeID); char openTypeChar = (char)('0' + openType.ordinal()); String cacheKey = openType != OpenType.LOCALE_DEFAULT_ROOT ? fullName + '#' + openTypeChar : fullName + '#' + openTypeChar + '#' + defaultID; return BUNDLE_CACHE.getInstance(cacheKey, new Loader() { @Override public ICUResourceBundle load() { if(DEBUG) System.out.println("Creating "+fullName); // here we assume that java type resource bundle organization // is required then the base name contains '.' else // the resource organization is of ICU type // so clients can instantiate resources of the type // com.mycompany.data.MyLocaleElements_en.res and // com.mycompany.data.MyLocaleElements.res // final String rootLocale = (baseName.indexOf('.')==-1) ? "root" : ""; String localeName = localeID.isEmpty() ? rootLocale : localeID; ICUResourceBundle b = ICUResourceBundle.createBundle(baseName, localeName, root); if(DEBUG)System.out.println("The bundle created is: "+b+" and openType="+openType+" and bundle.getNoFallback="+(b!=null && b.getNoFallback())); if (openType == OpenType.DIRECT || (b != null && b.getNoFallback())) { // no fallback because the caller said so or because the bundle says so // // TODO for b!=null: In C++, ures_openDirect() builds the parent chain // for its bundle unless its nofallback flag is set. // Otherwise we get test failures. // For example, item aliases are followed via ures_openDirect(), // and fail if the target bundle needs fallbacks but the chain is not set. // Figure out why Java does not build the parent chain // for a bundle that does not have nofallback. // Are the relevant test cases just disabled? // Do item aliases not get followed via "direct" loading? return b; } // fallback to locale ID parent if(b == null){ int i = localeName.lastIndexOf('_'); if (i != -1) { // Chop off the last underscore and the subtag after that. String temp = localeName.substring(0, i); b = instantiateBundle(baseName, temp, defaultID, root, openType); }else{ // No underscore, only a base language subtag. if(openType == OpenType.LOCALE_DEFAULT_ROOT && !localeIDStartsWithLangSubtag(defaultID, localeName)) { // Go to the default locale before root. b = instantiateBundle(baseName, defaultID, defaultID, root, openType); } else if(openType != OpenType.LOCALE_ONLY && !rootLocale.isEmpty()) { // Ultimately go to root. b = ICUResourceBundle.createBundle(baseName, rootLocale, root); } } }else{ UResourceBundle parent = null; localeName = b.getLocaleID(); int i = localeName.lastIndexOf('_'); // TODO: C++ uresbund.cpp also checks for %%ParentIsRoot. Why not Java? String parentLocaleName = ((ICUResourceBundleImpl.ResourceTable)b).findString("%%Parent"); if (parentLocaleName != null) { parent = instantiateBundle(baseName, parentLocaleName, defaultID, root, openType); } else if (i != -1) { parent = instantiateBundle(baseName, localeName.substring(0, i), defaultID, root, openType); } else if (!localeName.equals(rootLocale)){ parent = instantiateBundle(baseName, rootLocale, defaultID, root, openType); } if (!b.equals(parent)){ b.setParent(parent); } } return b; }}); } ICUResourceBundle get(String aKey, HashMap<String, String> aliasesVisited, UResourceBundle requested) { ICUResourceBundle obj = (ICUResourceBundle)handleGet(aKey, aliasesVisited, requested); if (obj == null) { obj = getParent(); if (obj != null) { //call the get method to recursively fetch the resource obj = obj.get(aKey, aliasesVisited, requested); } if (obj == null) { String fullName = ICUResourceBundleReader.getFullName(getBaseName(), getLocaleID()); throw new MissingResourceException( "Can't find resource for bundle " + fullName + ", key " + aKey, this.getClass().getName(), aKey); } } return obj; } /** Data member where the subclasses store the key. */ protected String key; /** * A resource word value that means "no resource". * Note: 0xffffffff == -1 * This has the same value as UResourceBundle.NONE, but they are semantically * different and should be used appropriately according to context: * NONE means "no type". * (The type of RES_BOGUS is RES_RESERVED=15 which was defined in ICU4C ures.h.) */ public static final int RES_BOGUS = 0xffffffff; //blic static final int RES_MAX_OFFSET = 0x0fffffff; /** * Resource type constant for aliases; * internally stores a string which identifies the actual resource * storing the data (can be in a different resource bundle). * Resolved internally before delivering the actual resource through the API. */ public static final int ALIAS = 3; /** Resource type constant for tables with 32-bit count, key offsets and values. */ public static final int TABLE32 = 4; /** * Resource type constant for tables with 16-bit count, key offsets and values. * All values are STRING_V2 strings. */ public static final int TABLE16 = 5; /** Resource type constant for 16-bit Unicode strings in formatVersion 2. */ public static final int STRING_V2 = 6; /** * Resource type constant for arrays with 16-bit count and values. * All values are STRING_V2 strings. */ public static final int ARRAY16 = 9; /* Resource type 15 is not defined but effectively used by RES_BOGUS=0xffffffff. */ /** * Create a bundle using a reader. * @param baseName The name for the bundle. * @param localeID The locale identification. * @param root The ClassLoader object root. * @return the new bundle */ public static ICUResourceBundle createBundle(String baseName, String localeID, ClassLoader root) { ICUResourceBundleReader reader = ICUResourceBundleReader.getReader(baseName, localeID, root); if (reader == null) { // could not open the .res file return null; } return getBundle(reader, baseName, localeID, root); } @Override protected String getLocaleID() { return wholeBundle.localeID; } @Override protected String getBaseName() { return wholeBundle.baseName; } @Override public ULocale getULocale() { return wholeBundle.ulocale; } /** * Returns true if this is the root bundle, or an item in the root bundle. */ public boolean isRoot() { return wholeBundle.localeID.isEmpty() || wholeBundle.localeID.equals("root"); } @Override public ICUResourceBundle getParent() { return (ICUResourceBundle) parent; } @Override protected void setParent(ResourceBundle parent) { this.parent = parent; } @Override public String getKey() { return key; } /** * Get the noFallback flag specified in the loaded bundle. * @return The noFallback flag. */ private boolean getNoFallback() { return wholeBundle.reader.getNoFallback(); } private static ICUResourceBundle getBundle(ICUResourceBundleReader reader, String baseName, String localeID, ClassLoader loader) { ICUResourceBundleImpl.ResourceTable rootTable; int rootRes = reader.getRootResource(); if(ICUResourceBundleReader.URES_IS_TABLE(ICUResourceBundleReader.RES_GET_TYPE(rootRes))) { WholeBundle wb = new WholeBundle(baseName, localeID, loader, reader); rootTable = new ICUResourceBundleImpl.ResourceTable(wb, rootRes); } else { throw new IllegalStateException("Invalid format error"); } String aliasString = rootTable.findString("%%ALIAS"); if(aliasString != null) { return (ICUResourceBundle)UResourceBundle.getBundleInstance(baseName, aliasString); } else { return rootTable; } } /** * Constructor for the root table of a bundle. */ protected ICUResourceBundle(WholeBundle wholeBundle) { this.wholeBundle = wholeBundle; } // constructor for inner classes protected ICUResourceBundle(ICUResourceBundle container, String key) { this.key = key; wholeBundle = container.wholeBundle; this.container = container; parent = container.parent; } private static final char RES_PATH_SEP_CHAR = '/'; private static final String RES_PATH_SEP_STR = "/"; private static final String ICUDATA = "ICUDATA"; private static final char HYPHEN = '-'; private static final String LOCALE = "LOCALE"; /** * Returns the resource object referred to from the alias _resource int's path string. * Throws MissingResourceException if not found. * * If the alias path does not contain a key path: * If keys != null then keys[:depth] is used. * Otherwise the base key path plus the key parameter is used. * * @param base A direct or indirect container of the alias. * @param keys The key path to the alias, or null. (const) * @param depth The length of the key path, if keys != null. * @param key The alias' own key within this current container, if keys == null. * @param _resource The alias resource int. * @param aliasesVisited Set of alias path strings already visited, for detecting loops. * We cannot change the type (e.g., to Set<String>) because it is used * in protected/@stable UResourceBundle methods. * @param requested The original resource object from which the lookup started, * which is the starting point for "/LOCALE/..." aliases. * @return the aliased resource object */ protected static ICUResourceBundle getAliasedResource( ICUResourceBundle base, String[] keys, int depth, String key, int _resource, HashMap<String, String> aliasesVisited, UResourceBundle requested) { WholeBundle wholeBundle = base.wholeBundle; ClassLoader loaderToUse = wholeBundle.loader; String locale; String keyPath = null; String bundleName; String rpath = wholeBundle.reader.getAlias(_resource); if (aliasesVisited == null) { aliasesVisited = new HashMap<String, String>(); } if (aliasesVisited.get(rpath) != null) { throw new IllegalArgumentException( "Circular references in the resource bundles"); } aliasesVisited.put(rpath, ""); if (rpath.indexOf(RES_PATH_SEP_CHAR) == 0) { int i = rpath.indexOf(RES_PATH_SEP_CHAR, 1); int j = rpath.indexOf(RES_PATH_SEP_CHAR, i + 1); bundleName = rpath.substring(1, i); if (j < 0) { locale = rpath.substring(i + 1); } else { locale = rpath.substring(i + 1, j); keyPath = rpath.substring(j + 1, rpath.length()); } //there is a path included if (bundleName.equals(ICUDATA)) { bundleName = ICUData.ICU_BASE_NAME; loaderToUse = ICU_DATA_CLASS_LOADER; }else if(bundleName.indexOf(ICUDATA)>-1){ int idx = bundleName.indexOf(HYPHEN); if(idx>-1){ bundleName = ICUData.ICU_BASE_NAME+RES_PATH_SEP_STR+bundleName.substring(idx+1,bundleName.length()); loaderToUse = ICU_DATA_CLASS_LOADER; } } } else { //no path start with locale int i = rpath.indexOf(RES_PATH_SEP_CHAR); if (i != -1) { locale = rpath.substring(0, i); keyPath = rpath.substring(i + 1); } else { locale = rpath; } bundleName = wholeBundle.baseName; } ICUResourceBundle bundle = null; ICUResourceBundle sub = null; if(bundleName.equals(LOCALE)){ bundleName = wholeBundle.baseName; keyPath = rpath.substring(LOCALE.length() + 2/* prepending and appending / */, rpath.length()); // Get the top bundle of the requested bundle bundle = (ICUResourceBundle)requested; while (bundle.container != null) { bundle = bundle.container; } sub = ICUResourceBundle.findResourceWithFallback(keyPath, bundle, null); }else{ bundle = getBundleInstance(bundleName, locale, loaderToUse, false); int numKeys; if (keyPath != null) { numKeys = countPathKeys(keyPath); if (numKeys > 0) { keys = new String[numKeys]; getResPathKeys(keyPath, numKeys, keys, 0); } } else if (keys != null) { numKeys = depth; } else { depth = base.getResDepth(); numKeys = depth + 1; keys = new String[numKeys]; base.getResPathKeys(keys, depth); keys[depth] = key; } if (numKeys > 0) { sub = bundle; for (int i = 0; sub != null && i < numKeys; ++i) { sub = sub.get(keys[i], aliasesVisited, requested); } } } if (sub == null) { throw new MissingResourceException(wholeBundle.localeID, wholeBundle.baseName, key); } // TODO: If we know that sub is not cached, // then we should set its container and key to the alias' location, // so that it behaves as if its value had been copied into the alias location. // However, findResourceWithFallback() must reroute its bundle and key path // to where the alias data comes from. return sub; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public final Set<String> getTopLevelKeySet() { return wholeBundle.topLevelKeys; } /** * @internal * @deprecated This API is ICU internal only. */ @Deprecated public final void setTopLevelKeySet(Set<String> keySet) { wholeBundle.topLevelKeys = keySet; } // This is the worker function for the public getKeys(). // TODO: Now that UResourceBundle uses handleKeySet(), this function is obsolete. // It is also not inherited from ResourceBundle, and it is not implemented // by ResourceBundleWrapper despite its documentation requiring all subclasses to // implement it. // Consider deprecating UResourceBundle.handleGetKeys(), and consider making it always return null. @Override protected Enumeration<String> handleGetKeys() { return Collections.enumeration(handleKeySet()); } @Override protected boolean isTopLevelResource() { return container == null; } }