com.ibm.icu.impl.ICUResourceBundle Maven / Gradle / Ivy
Show all versions of virtdata-lib-realer Show documentation
// © 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 topLevelKeys;
}
WholeBundle wholeBundle;
private ICUResourceBundle container;
/** Loader for bundle instances, for caching. */
private static abstract class Loader {
abstract ICUResourceBundle load();
}
private static CacheBase BUNDLE_CACHE =
new SoftCache() {
@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 keywords = new HashSet();
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 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:
* UResourceBundle bundle = UResourceBundle.getBundleInstance("de__PHONEBOOK");
* ICUResourceBundle result = null;
* if(bundle instanceof ICUResourceBundle){
* result = ((ICUResourceBundle) bundle).getWithFallback("collations/default");
* }
*
*
* @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 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 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 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 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 not 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 list = new ArrayList(ulocales.length);
HashSet uniqueSet = new HashSet();
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 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 names) {
java.security.AccessController
.doPrivileged(new java.security.PrivilegedAction() {
@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 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 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 createFullLocaleNameSet(String baseName, ClassLoader loader) {
String bn = baseName.endsWith("/") ? baseName : baseName + "/";
Set set = new HashSet();
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 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 createLocaleNameSet(String baseName, ClassLoader loader) {
HashSet set = new HashSet();
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 nameSet;
private volatile Set 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 getLocaleNameSet() {
if (nameSet == null) {
synchronized(this) {
if (nameSet == null) {
nameSet = createLocaleNameSet(prefix, loader);
}
}
}
return nameSet;
}
Set 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 GET_AVAILABLE_CACHE =
new SoftCache() {
@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.
*
* 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.
*
*
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.
*
*
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.
*
*
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 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) 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 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();
}
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 getTopLevelKeySet() {
return wholeBundle.topLevelKeys;
}
/**
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public final void setTopLevelKeySet(Set 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 handleGetKeys() {
return Collections.enumeration(handleKeySet());
}
@Override
protected boolean isTopLevelResource() {
return container == null;
}
}