1 // © 2016 and later: Unicode, Inc. and others. 2 // License & terms of use: http://www.unicode.org/copyright.html 3 /* 4 * ***************************************************************************** 5 * Copyright (C) 2005-2016, International Business Machines Corporation and 6 * others. All Rights Reserved. 7 * ***************************************************************************** 8 */ 9 10 package com.ibm.icu.impl; 11 12 import java.io.BufferedReader; 13 import java.io.IOException; 14 import java.io.InputStream; 15 import java.io.InputStreamReader; 16 import java.net.URL; 17 import java.util.ArrayList; 18 import java.util.Collections; 19 import java.util.Comparator; 20 import java.util.EnumMap; 21 import java.util.Enumeration; 22 import java.util.HashMap; 23 import java.util.HashSet; 24 import java.util.Iterator; 25 import java.util.Locale; 26 import java.util.MissingResourceException; 27 import java.util.ResourceBundle; 28 import java.util.Set; 29 30 import com.ibm.icu.impl.ICUResourceBundleReader.ReaderValue; 31 import com.ibm.icu.impl.URLHandler.URLVisitor; 32 import com.ibm.icu.util.ULocale; 33 import com.ibm.icu.util.UResourceBundle; 34 import com.ibm.icu.util.UResourceBundleIterator; 35 import com.ibm.icu.util.UResourceTypeMismatchException; 36 37 public class ICUResourceBundle extends UResourceBundle { 38 /** 39 * CLDR string value "∅∅∅" prevents fallback to the parent bundle. 40 */ 41 public static final String NO_INHERITANCE_MARKER = "\u2205\u2205\u2205"; 42 43 /** 44 * The class loader constant to be used with getBundleInstance API 45 */ 46 public static final ClassLoader ICU_DATA_CLASS_LOADER = ClassLoaderUtil.getClassLoader(ICUData.class); 47 48 /** 49 * The name of the resource containing the installed locales 50 */ 51 protected static final String INSTALLED_LOCALES = "InstalledLocales"; 52 53 /** 54 * Fields for a whole bundle, rather than any specific resource in the bundle. 55 * Corresponds roughly to ICU4C/source/common/uresimp.h struct UResourceDataEntry. 56 */ 57 protected static final class WholeBundle { WholeBundle(String baseName, String localeID, ClassLoader loader, ICUResourceBundleReader reader)58 WholeBundle(String baseName, String localeID, ClassLoader loader, 59 ICUResourceBundleReader reader) { 60 this.baseName = baseName; 61 this.localeID = localeID; 62 this.ulocale = new ULocale(localeID); 63 this.loader = loader; 64 this.reader = reader; 65 } 66 67 String baseName; 68 String localeID; 69 ULocale ulocale; 70 ClassLoader loader; 71 72 /** 73 * Access to the bits and bytes of the resource bundle. 74 * Hides low-level details. 75 */ 76 ICUResourceBundleReader reader; 77 78 // TODO: Remove topLevelKeys when we upgrade to Java 6 where ResourceBundle caches the keySet(). 79 Set<String> topLevelKeys; 80 } 81 82 WholeBundle wholeBundle; 83 private ICUResourceBundle container; 84 85 /** Loader for bundle instances, for caching. */ 86 private static abstract class Loader { load()87 abstract ICUResourceBundle load(); 88 } 89 90 private static CacheBase<String, ICUResourceBundle, Loader> BUNDLE_CACHE = 91 new SoftCache<String, ICUResourceBundle, Loader>() { 92 @Override 93 protected ICUResourceBundle createInstance(String unusedKey, Loader loader) { 94 return loader.load(); 95 } 96 }; 97 98 /** 99 * Returns a functionally equivalent locale, considering keywords as well, for the specified keyword. 100 * @param baseName resource specifier 101 * @param resName top level resource to consider (such as "collations") 102 * @param keyword a particular keyword to consider (such as "collation" ) 103 * @param locID The requested locale 104 * @param isAvailable If non-null, 1-element array of fillin parameter that indicates whether the 105 * requested locale was available. The locale is defined as 'available' if it physically 106 * exists within the specified tree and included in 'InstalledLocales'. 107 * @param omitDefault if true, omit keyword and value if default. 108 * 'de_DE\@collation=standard' -> 'de_DE' 109 * @return the locale 110 * @internal ICU 3.0 111 */ getFunctionalEquivalent(String baseName, ClassLoader loader, String resName, String keyword, ULocale locID, boolean isAvailable[], boolean omitDefault)112 public static final ULocale getFunctionalEquivalent(String baseName, ClassLoader loader, 113 String resName, String keyword, ULocale locID, 114 boolean isAvailable[], boolean omitDefault) { 115 String kwVal = locID.getKeywordValue(keyword); 116 String baseLoc = locID.getBaseName(); 117 String defStr = null; 118 ULocale parent = new ULocale(baseLoc); 119 ULocale defLoc = null; // locale where default (found) resource is 120 boolean lookForDefault = false; // true if kwVal needs to be set 121 ULocale fullBase = null; // base locale of found (target) resource 122 int defDepth = 0; // depth of 'default' marker 123 int resDepth = 0; // depth of found resource; 124 125 if ((kwVal == null) || (kwVal.length() == 0) 126 || kwVal.equals(DEFAULT_TAG)) { 127 kwVal = ""; // default tag is treated as no keyword 128 lookForDefault = true; 129 } 130 131 // Check top level locale first 132 ICUResourceBundle r = null; 133 134 r = (ICUResourceBundle) UResourceBundle.getBundleInstance(baseName, parent); 135 if (isAvailable != null) { 136 isAvailable[0] = false; 137 ULocale[] availableULocales = getAvailEntry(baseName, loader) 138 .getULocaleList(ULocale.AvailableType.DEFAULT); 139 for (int i = 0; i < availableULocales.length; i++) { 140 if (parent.equals(availableULocales[i])) { 141 isAvailable[0] = true; 142 break; 143 } 144 } 145 } 146 // determine in which locale (if any) the currently relevant 'default' is 147 do { 148 try { 149 ICUResourceBundle irb = (ICUResourceBundle) r.get(resName); 150 defStr = irb.getString(DEFAULT_TAG); 151 if (lookForDefault == true) { 152 kwVal = defStr; 153 lookForDefault = false; 154 } 155 defLoc = r.getULocale(); 156 } catch (MissingResourceException t) { 157 // Ignore error and continue search. 158 } 159 if (defLoc == null) { 160 r = r.getParent(); 161 defDepth++; 162 } 163 } while ((r != null) && (defLoc == null)); 164 165 // Now, search for the named resource 166 parent = new ULocale(baseLoc); 167 r = (ICUResourceBundle) UResourceBundle.getBundleInstance(baseName, parent); 168 // determine in which locale (if any) the named resource is located 169 do { 170 try { 171 ICUResourceBundle irb = (ICUResourceBundle)r.get(resName); 172 /* UResourceBundle urb = */irb.get(kwVal); 173 fullBase = irb.getULocale(); 174 // If the get() completed, we have the full base locale 175 // If we fell back to an ancestor of the old 'default', 176 // we need to re calculate the "default" keyword. 177 if ((fullBase != null) && ((resDepth) > defDepth)) { 178 defStr = irb.getString(DEFAULT_TAG); 179 defLoc = r.getULocale(); 180 defDepth = resDepth; 181 } 182 } catch (MissingResourceException t) { 183 // Ignore error, 184 } 185 if (fullBase == null) { 186 r = r.getParent(); 187 resDepth++; 188 } 189 } while ((r != null) && (fullBase == null)); 190 191 if (fullBase == null && // Could not find resource 'kwVal' 192 (defStr != null) && // default was defined 193 !defStr.equals(kwVal)) { // kwVal is not default 194 // couldn't find requested resource. Fall back to default. 195 kwVal = defStr; // Fall back to default. 196 parent = new ULocale(baseLoc); 197 r = (ICUResourceBundle) UResourceBundle.getBundleInstance(baseName, parent); 198 resDepth = 0; 199 // determine in which locale (if any) the named resource is located 200 do { 201 try { 202 ICUResourceBundle irb = (ICUResourceBundle)r.get(resName); 203 ICUResourceBundle urb = (ICUResourceBundle)irb.get(kwVal); 204 205 // if we didn't fail before this.. 206 fullBase = r.getULocale(); 207 208 // If the fetched item (urb) is in a different locale than our outer locale (r/fullBase) 209 // then we are in a 'fallback' situation. treat as a missing resource situation. 210 if(!fullBase.getBaseName().equals(urb.getULocale().getBaseName())) { 211 fullBase = null; // fallback condition. Loop and try again. 212 } 213 214 // If we fell back to an ancestor of the old 'default', 215 // we need to re calculate the "default" keyword. 216 if ((fullBase != null) && ((resDepth) > defDepth)) { 217 defStr = irb.getString(DEFAULT_TAG); 218 defLoc = r.getULocale(); 219 defDepth = resDepth; 220 } 221 } catch (MissingResourceException t) { 222 // Ignore error, continue search. 223 } 224 if (fullBase == null) { 225 r = r.getParent(); 226 resDepth++; 227 } 228 } while ((r != null) && (fullBase == null)); 229 } 230 231 if (fullBase == null) { 232 throw new MissingResourceException( 233 "Could not find locale containing requested or default keyword.", 234 baseName, keyword + "=" + kwVal); 235 } 236 237 if (omitDefault 238 && defStr.equals(kwVal) // if default was requested and 239 && resDepth <= defDepth) { // default was set in same locale or child 240 return fullBase; // Keyword value is default - no keyword needed in locale 241 } else { 242 return new ULocale(fullBase.getBaseName() + "@" + keyword + "=" + kwVal); 243 } 244 } 245 246 /** 247 * Given a tree path and keyword, return a string enumeration of all possible values for that keyword. 248 * @param baseName resource specifier 249 * @param keyword a particular keyword to consider, must match a top level resource name 250 * within the tree. (i.e. "collations") 251 * @internal ICU 3.0 252 */ getKeywordValues(String baseName, String keyword)253 public static final String[] getKeywordValues(String baseName, String keyword) { 254 Set<String> keywords = new HashSet<>(); 255 ULocale locales[] = getAvailEntry(baseName, ICU_DATA_CLASS_LOADER) 256 .getULocaleList(ULocale.AvailableType.DEFAULT); 257 int i; 258 259 for (i = 0; i < locales.length; i++) { 260 try { 261 UResourceBundle b = UResourceBundle.getBundleInstance(baseName, locales[i]); 262 // downcast to ICUResourceBundle? 263 ICUResourceBundle irb = (ICUResourceBundle) (b.getObject(keyword)); 264 Enumeration<String> e = irb.getKeys(); 265 while (e.hasMoreElements()) { 266 String s = e.nextElement(); 267 if (!DEFAULT_TAG.equals(s) && !s.startsWith("private-")) { 268 // don't add 'default' items, nor unlisted types 269 keywords.add(s); 270 } 271 } 272 } catch (Throwable t) { 273 //System.err.println("Error in - " + new Integer(i).toString() 274 // + " - " + t.toString()); 275 // ignore the err - just skip that resource 276 } 277 } 278 return keywords.toArray(new String[0]); 279 } 280 281 /** 282 * This method performs multilevel fallback for fetching items from the 283 * bundle e.g: If resource is in the form de__PHONEBOOK{ collations{ 284 * default{ "phonebook"} } } If the value of "default" key needs to be 285 * accessed, then do: <code> 286 * UResourceBundle bundle = UResourceBundle.getBundleInstance("de__PHONEBOOK"); 287 * ICUResourceBundle result = null; 288 * if(bundle instanceof ICUResourceBundle){ 289 * result = ((ICUResourceBundle) bundle).getWithFallback("collations/default"); 290 * } 291 * </code> 292 * 293 * @param path The path to the required resource key 294 * @return resource represented by the key 295 * @exception MissingResourceException If a resource was not found. 296 */ getWithFallback(String path)297 public ICUResourceBundle getWithFallback(String path) throws MissingResourceException { 298 ICUResourceBundle actualBundle = this; 299 300 // now recurse to pick up sub levels of the items 301 ICUResourceBundle result = findResourceWithFallback(path, actualBundle, null); 302 303 if (result == null) { 304 throw new MissingResourceException( 305 "Can't find resource for bundle " 306 + this.getClass().getName() + ", key " + getType(), 307 path, getKey()); 308 } 309 310 if (result.getType() == STRING && result.getString().equals(NO_INHERITANCE_MARKER)) { 311 throw new MissingResourceException("Encountered NO_INHERITANCE_MARKER", path, getKey()); 312 } 313 314 return result; 315 } 316 at(int index)317 public ICUResourceBundle at(int index) { 318 return (ICUResourceBundle) handleGet(index, null, this); 319 } 320 at(String key)321 public ICUResourceBundle at(String key) { 322 // don't ever presume the key is an int in disguise, like ResourceArray does. 323 if (this instanceof ICUResourceBundleImpl.ResourceTable) { 324 return (ICUResourceBundle) handleGet(key, null, this); 325 } 326 return null; 327 } 328 329 @Override findTopLevel(int index)330 public ICUResourceBundle findTopLevel(int index) { 331 return (ICUResourceBundle) super.findTopLevel(index); 332 } 333 334 @Override findTopLevel(String aKey)335 public ICUResourceBundle findTopLevel(String aKey) { 336 return (ICUResourceBundle) super.findTopLevel(aKey); 337 } 338 339 /** 340 * Like getWithFallback, but returns null if the resource is not found instead of 341 * throwing an exception. 342 * @param path the path to the resource 343 * @return the resource, or null 344 */ findWithFallback(String path)345 public ICUResourceBundle findWithFallback(String path) { 346 return findResourceWithFallback(path, this, null); 347 } findStringWithFallback(String path)348 public String findStringWithFallback(String path) { 349 return findStringWithFallback(path, this, null); 350 } 351 352 // will throw type mismatch exception if the resource is not a string getStringWithFallback(String path)353 public String getStringWithFallback(String path) throws MissingResourceException { 354 // Optimized form of getWithFallback(path).getString(); 355 ICUResourceBundle actualBundle = this; 356 String result = findStringWithFallback(path, actualBundle, null); 357 358 if (result == null) { 359 throw new MissingResourceException( 360 "Can't find resource for bundle " 361 + this.getClass().getName() + ", key " + getType(), 362 path, getKey()); 363 } 364 365 if (result.equals(NO_INHERITANCE_MARKER)) { 366 throw new MissingResourceException("Encountered NO_INHERITANCE_MARKER", path, getKey()); 367 } 368 return result; 369 } 370 getValueWithFallback(String path)371 public UResource.Value getValueWithFallback(String path) throws MissingResourceException { 372 ICUResourceBundle rb; 373 if (path.isEmpty()) { 374 rb = this; 375 } else { 376 rb = findResourceWithFallback(path, this, null); 377 if (rb == null) { 378 throw new MissingResourceException( 379 "Can't find resource for bundle " 380 + this.getClass().getName() + ", key " + getType(), 381 path, getKey()); 382 } 383 } 384 ReaderValue readerValue = new ReaderValue(); 385 ICUResourceBundleImpl impl = (ICUResourceBundleImpl)rb; 386 readerValue.reader = impl.wholeBundle.reader; 387 readerValue.res = impl.getResource(); 388 return readerValue; 389 } 390 getAllItemsWithFallbackNoFail(String path, UResource.Sink sink)391 public void getAllItemsWithFallbackNoFail(String path, UResource.Sink sink) { 392 try { 393 getAllItemsWithFallback(path, sink); 394 } catch (MissingResourceException e) { 395 // Quietly ignore the exception. 396 } 397 } 398 399 /** 400 * Locates the resource specified by `path` in this resource bundle (performing any necessary fallback and 401 * following any aliases) and calls the specified `sink`'s `put()` method with that resource. Then walks the 402 * bundle's parent chain, calling `put()` on the sink for each item in the parent chain. 403 * @param path The path of the desired resource 404 * @param sink A `UResource.Sink` that gets called for each resource in the parent chain 405 */ getAllItemsWithFallback(String path, UResource.Sink sink)406 public void getAllItemsWithFallback(String path, UResource.Sink sink) 407 throws MissingResourceException { 408 // Collect existing and parsed key objects into an array of keys, 409 // rather than assembling and parsing paths. 410 int numPathKeys = countPathKeys(path); // How much deeper does the path go? 411 ICUResourceBundle rb; 412 if (numPathKeys == 0) { 413 rb = this; 414 } else { 415 // Get the keys for finding the target. 416 int depth = getResDepth(); // How deep are we in this bundle? 417 String[] pathKeys = new String[depth + numPathKeys]; 418 getResPathKeys(path, numPathKeys, pathKeys, depth); 419 rb = findResourceWithFallback(pathKeys, depth, this, null); 420 if (rb == null) { 421 throw new MissingResourceException( 422 "Can't find resource for bundle " 423 + this.getClass().getName() + ", key " + getType(), 424 path, getKey()); 425 } 426 } 427 UResource.Key key = new UResource.Key(); 428 ReaderValue readerValue = new ReaderValue(); 429 rb.getAllItemsWithFallback(key, readerValue, sink, this); 430 } 431 432 /** 433 * Locates the resource specified by `path` in this resource bundle (performing any necessary fallback and 434 * following any aliases) and, if the resource is a table resource, iterates over its immediate child resources (again, 435 * following any aliases to get the individual resource values), and calls the specified `sink`'s `put()` method 436 * for each child resource (passing it that resource's key and either its actual value or, if that value is an 437 * alias, the value you get by following the alias). Then walks back over the bundle's parent chain, 438 * similarly iterating over each parent table resource's child resources. 439 * Does not descend beyond one level of table children. 440 * @param path The path of the desired resource 441 * @param sink A `UResource.Sink` that gets called for each child resource of the specified resource (and each child 442 * of the resources in its parent chain). 443 */ getAllChildrenWithFallback(final String path, final UResource.Sink sink)444 public void getAllChildrenWithFallback(final String path, final UResource.Sink sink) 445 throws MissingResourceException { 446 class AllChildrenSink extends UResource.Sink { 447 @Override 448 public void put(UResource.Key key, UResource.Value value, boolean noFallback) { 449 UResource.Table itemsTable = value.getTable(); 450 for (int i = 0; itemsTable.getKeyAndValue(i, key, value); ++i) { 451 if (value.getType() == ALIAS) { 452 // if the current entry in the table is an alias, re-fetch it using getAliasedResource(): 453 // this will follow the alias (and any aliases it points to) and bring back the real value 454 String aliasPath = value.getAliasString(); 455 ICUResourceBundle aliasedResource = getAliasedResource(aliasPath, wholeBundle.loader, 456 "", null, 0, null, 457 null, ICUResourceBundle.this); 458 ICUResourceBundleImpl aliasedResourceImpl = (ICUResourceBundleImpl)aliasedResource; 459 ReaderValue aliasedValue = new ReaderValue(); 460 aliasedValue.reader = aliasedResourceImpl.wholeBundle.reader; 461 aliasedValue.res = aliasedResourceImpl.getResource(); 462 sink.put(key, aliasedValue, noFallback); 463 } else { 464 sink.put(key, value, noFallback); 465 } 466 } 467 } 468 } 469 470 getAllItemsWithFallback(path, new AllChildrenSink()); 471 } 472 getAllItemsWithFallback( UResource.Key key, ReaderValue readerValue, UResource.Sink sink, UResourceBundle requested)473 private void getAllItemsWithFallback( 474 UResource.Key key, ReaderValue readerValue, UResource.Sink sink, UResourceBundle requested) { 475 // We recursively enumerate child-first, 476 // only storing parent items in the absence of child items. 477 // The sink needs to store a placeholder value for the no-fallback/no-inheritance marker 478 // to prevent a parent item from being stored. 479 // 480 // It would be possible to recursively enumerate parent-first, 481 // overriding parent items with child items. 482 // When the sink sees the no-fallback/no-inheritance marker, 483 // then it would remove the parent's item. 484 // We would deserialize parent values even though they are overridden in a child bundle. 485 ICUResourceBundleImpl impl = (ICUResourceBundleImpl)this; 486 readerValue.reader = impl.wholeBundle.reader; 487 readerValue.res = impl.getResource(); 488 key.setString(this.key != null ? this.key : ""); 489 sink.put(key, readerValue, parent == null); 490 if (parent != null) { 491 // We might try to query the sink whether 492 // any fallback from the parent bundle is still possible. 493 ICUResourceBundle parentBundle = (ICUResourceBundle)parent; 494 ICUResourceBundle rb; 495 int depth = getResDepth(); 496 if (depth == 0) { 497 rb = parentBundle; 498 } else { 499 // Re-fetch the path keys: They may differ from the original ones 500 // if we had followed an alias. 501 String[] pathKeys = new String[depth]; 502 getResPathKeys(pathKeys, depth); 503 rb = findResourceWithFallback(pathKeys, 0, parentBundle, requested); 504 } 505 if (rb != null) { 506 rb.getAllItemsWithFallback(key, readerValue, sink, requested); 507 } 508 } 509 } 510 511 /** 512 * Return a set of the locale names supported by a collection of resource 513 * bundles. 514 * 515 * @param bundlePrefix the prefix of the resource bundles to use. 516 */ getAvailableLocaleNameSet(String bundlePrefix, ClassLoader loader)517 public static Set<String> getAvailableLocaleNameSet(String bundlePrefix, ClassLoader loader) { 518 return getAvailEntry(bundlePrefix, loader).getLocaleNameSet(); 519 } 520 521 /** 522 * Return a set of all the locale names supported by a collection of 523 * resource bundles. 524 */ getFullLocaleNameSet()525 public static Set<String> getFullLocaleNameSet() { 526 return getFullLocaleNameSet(ICUData.ICU_BASE_NAME, ICU_DATA_CLASS_LOADER); 527 } 528 529 /** 530 * Return a set of all the locale names supported by a collection of 531 * resource bundles. 532 * 533 * @param bundlePrefix the prefix of the resource bundles to use. 534 */ getFullLocaleNameSet(String bundlePrefix, ClassLoader loader)535 public static Set<String> getFullLocaleNameSet(String bundlePrefix, ClassLoader loader) { 536 return getAvailEntry(bundlePrefix, loader).getFullLocaleNameSet(); 537 } 538 539 /** 540 * Return a set of the locale names supported by a collection of resource 541 * bundles. 542 */ getAvailableLocaleNameSet()543 public static Set<String> getAvailableLocaleNameSet() { 544 return getAvailableLocaleNameSet(ICUData.ICU_BASE_NAME, ICU_DATA_CLASS_LOADER); 545 } 546 547 /** 548 * Get the set of Locales installed in the specified bundles, for the specified type. 549 * @return the list of available locales 550 */ getAvailableULocales(String baseName, ClassLoader loader, ULocale.AvailableType type)551 public static final ULocale[] getAvailableULocales(String baseName, ClassLoader loader, 552 ULocale.AvailableType type) { 553 return getAvailEntry(baseName, loader).getULocaleList(type); 554 } 555 556 /** 557 * Get the set of ULocales installed the base bundle. 558 * @return the list of available locales 559 */ getAvailableULocales()560 public static final ULocale[] getAvailableULocales() { 561 return getAvailableULocales(ICUData.ICU_BASE_NAME, ICU_DATA_CLASS_LOADER, ULocale.AvailableType.DEFAULT); 562 } 563 564 /** 565 * Get the set of ULocales installed the base bundle, for the specified type. 566 * @return the list of available locales for the specified type 567 */ getAvailableULocales(ULocale.AvailableType type)568 public static final ULocale[] getAvailableULocales(ULocale.AvailableType type) { 569 return getAvailableULocales(ICUData.ICU_BASE_NAME, ICU_DATA_CLASS_LOADER, type); 570 } 571 572 /** 573 * Get the set of Locales installed in the specified bundles. 574 * @return the list of available locales 575 */ getAvailableULocales(String baseName, ClassLoader loader)576 public static final ULocale[] getAvailableULocales(String baseName, ClassLoader loader) { 577 return getAvailableULocales(baseName, loader, ULocale.AvailableType.DEFAULT); 578 } 579 580 /** 581 * Get the set of Locales installed in the specified bundles, for the specified type. 582 * @return the list of available locales 583 */ getAvailableLocales(String baseName, ClassLoader loader, ULocale.AvailableType type)584 public static final Locale[] getAvailableLocales(String baseName, ClassLoader loader, 585 ULocale.AvailableType type) { 586 return getAvailEntry(baseName, loader).getLocaleList(type); 587 } 588 589 /** 590 * Get the set of ULocales installed the base bundle. 591 * @return the list of available locales 592 */ getAvailableLocales()593 public static final Locale[] getAvailableLocales() { 594 return getAvailableLocales(ICUData.ICU_BASE_NAME, ICU_DATA_CLASS_LOADER, ULocale.AvailableType.DEFAULT); 595 } 596 597 /** 598 * Get the set of Locales installed the base bundle, for the specified type. 599 * @return the list of available locales 600 */ getAvailableLocales(ULocale.AvailableType type)601 public static final Locale[] getAvailableLocales(ULocale.AvailableType type) { 602 return getAvailableLocales(ICUData.ICU_BASE_NAME, ICU_DATA_CLASS_LOADER, type); 603 } 604 605 /** 606 * Get the set of Locales installed in the specified bundles. 607 * @return the list of available locales 608 */ getAvailableLocales(String baseName, ClassLoader loader)609 public static final Locale[] getAvailableLocales(String baseName, ClassLoader loader) { 610 return getAvailableLocales(baseName, loader, ULocale.AvailableType.DEFAULT); 611 } 612 613 /** 614 * Convert a list of ULocales to a list of Locales. ULocales with a script code will not be converted 615 * since they cannot be represented as a Locale. This means that the two lists will <b>not</b> match 616 * one-to-one, and that the returned list might be shorter than the input list. 617 * @param ulocales a list of ULocales to convert to a list of Locales. 618 * @return the list of converted ULocales 619 */ getLocaleList(ULocale[] ulocales)620 public static final Locale[] getLocaleList(ULocale[] ulocales) { 621 ArrayList<Locale> list = new ArrayList<>(ulocales.length); 622 HashSet<Locale> uniqueSet = new HashSet<>(); 623 for (int i = 0; i < ulocales.length; i++) { 624 Locale loc = ulocales[i].toLocale(); 625 if (!uniqueSet.contains(loc)) { 626 list.add(loc); 627 uniqueSet.add(loc); 628 } 629 } 630 return list.toArray(new Locale[list.size()]); 631 } 632 633 /** 634 * Returns the locale of this resource bundle. This method can be used after 635 * a call to getBundle() to determine whether the resource bundle returned 636 * really corresponds to the requested locale or is a fallback. 637 * 638 * @return the locale of this resource bundle 639 */ 640 @Override getLocale()641 public Locale getLocale() { 642 return getULocale().toLocale(); 643 } 644 645 646 // ========== privates ========== 647 private static final String ICU_RESOURCE_INDEX = "res_index"; 648 649 private static final String DEFAULT_TAG = "default"; 650 651 // The name of text file generated by ICU4J build script including all locale names 652 // (canonical, alias and root) 653 private static final String FULL_LOCALE_NAMES_LIST = "fullLocaleNames.lst"; 654 655 // Flag for enabling/disabling debugging code 656 private static final boolean DEBUG = ICUDebug.enabled("localedata"); 657 658 private static final class AvailableLocalesSink extends UResource.Sink { 659 660 EnumMap<ULocale.AvailableType, ULocale[]> output; 661 AvailableLocalesSink(EnumMap<ULocale.AvailableType, ULocale[]> output)662 public AvailableLocalesSink(EnumMap<ULocale.AvailableType, ULocale[]> output) { 663 this.output = output; 664 } 665 666 @Override put(UResource.Key key, UResource.Value value, boolean noFallback)667 public void put(UResource.Key key, UResource.Value value, boolean noFallback) { 668 UResource.Table resIndexTable = value.getTable(); 669 for (int i = 0; resIndexTable.getKeyAndValue(i, key, value); ++i) { 670 ULocale.AvailableType type; 671 if (key.contentEquals("InstalledLocales")) { 672 type = ULocale.AvailableType.DEFAULT; 673 } else if (key.contentEquals("AliasLocales")) { 674 type = ULocale.AvailableType.ONLY_LEGACY_ALIASES; 675 } else { 676 // CLDRVersion, etc. 677 continue; 678 } 679 UResource.Table availableLocalesTable = value.getTable(); 680 ULocale[] locales = new ULocale[availableLocalesTable.getSize()]; 681 for (int j = 0; availableLocalesTable.getKeyAndValue(j, key, value); ++j) { 682 locales[j] = new ULocale(key.toString()); 683 } 684 output.put(type, locales); 685 } 686 } 687 } 688 createULocaleList( String baseName, ClassLoader root)689 private static final EnumMap<ULocale.AvailableType, ULocale[]> createULocaleList( 690 String baseName, ClassLoader root) { 691 // the canned list is a subset of all the available .res files, the idea 692 // is we don't export them 693 // all. gotta be a better way to do this, since to add a locale you have 694 // to update this list, 695 // and it's embedded in our binary resources. 696 ICUResourceBundle rb = (ICUResourceBundle) UResourceBundle.instantiateBundle(baseName, ICU_RESOURCE_INDEX, root, true); 697 698 EnumMap<ULocale.AvailableType, ULocale[]> result = new EnumMap<>(ULocale.AvailableType.class); 699 AvailableLocalesSink sink = new AvailableLocalesSink(result); 700 rb.getAllItemsWithFallback("", sink); 701 return result; 702 } 703 704 // Same as createULocaleList() but catches the MissingResourceException 705 // and returns the data in a different form. addLocaleIDsFromIndexBundle(String baseName, ClassLoader root, Set<String> locales)706 private static final void addLocaleIDsFromIndexBundle(String baseName, 707 ClassLoader root, Set<String> locales) { 708 ICUResourceBundle bundle; 709 try { 710 bundle = (ICUResourceBundle) UResourceBundle.instantiateBundle(baseName, ICU_RESOURCE_INDEX, root, true); 711 bundle = (ICUResourceBundle) bundle.get(INSTALLED_LOCALES); 712 } catch (MissingResourceException e) { 713 if (DEBUG) { 714 System.out.println("couldn't find " + baseName + '/' + ICU_RESOURCE_INDEX + ".res"); 715 Thread.dumpStack(); 716 } 717 return; 718 } 719 UResourceBundleIterator iter = bundle.getIterator(); 720 iter.reset(); 721 while (iter.hasNext()) { 722 String locstr = iter.next(). getKey(); 723 locales.add(locstr); 724 } 725 } 726 addBundleBaseNamesFromClassLoader( final String bn, final ClassLoader root, final Set<String> names)727 private static final void addBundleBaseNamesFromClassLoader( 728 final String bn, final ClassLoader root, final Set<String> names) { 729 java.security.AccessController 730 .doPrivileged(new java.security.PrivilegedAction<Void>() { 731 @Override 732 public Void run() { 733 try { 734 // bn has a trailing slash: The WebSphere class loader would return null 735 // for a raw directory name without it. 736 Enumeration<URL> urls = root.getResources(bn); 737 if (urls == null) { 738 return null; 739 } 740 URLVisitor v = new URLVisitor() { 741 @Override 742 public void visit(String s) { 743 if (s.endsWith(".res")) { 744 String locstr = s.substring(0, s.length() - 4); 745 names.add(locstr); 746 } 747 } 748 }; 749 while (urls.hasMoreElements()) { 750 URL url = urls.nextElement(); 751 URLHandler handler = URLHandler.get(url); 752 if (handler != null) { 753 handler.guide(v, false); 754 } else { 755 if (DEBUG) System.out.println("handler for " + url + " is null"); 756 } 757 } 758 } catch (IOException e) { 759 if (DEBUG) System.out.println("ouch: " + e.getMessage()); 760 } 761 return null; 762 } 763 }); 764 } 765 addLocaleIDsFromListFile(String bn, ClassLoader root, Set<String> locales)766 private static void addLocaleIDsFromListFile(String bn, ClassLoader root, Set<String> locales) { 767 try { 768 InputStream s = root.getResourceAsStream(bn + FULL_LOCALE_NAMES_LIST); 769 if (s != null) { 770 BufferedReader br = new BufferedReader(new InputStreamReader(s, "ASCII")); 771 try { 772 String line; 773 while ((line = br.readLine()) != null) { 774 if (line.length() != 0 && !line.startsWith("#")) { 775 locales.add(line); 776 } 777 } 778 } 779 finally { 780 br.close(); 781 } 782 } 783 } catch (IOException ignored) { 784 // swallow it 785 } 786 } 787 createFullLocaleNameSet(String baseName, ClassLoader loader)788 private static Set<String> createFullLocaleNameSet(String baseName, ClassLoader loader) { 789 String bn = baseName.endsWith("/") ? baseName : baseName + "/"; 790 Set<String> set = new HashSet<>(); 791 String skipScan = ICUConfig.get("com.ibm.icu.impl.ICUResourceBundle.skipRuntimeLocaleResourceScan", "false"); 792 if (!skipScan.equalsIgnoreCase("true")) { 793 // scan available locale resources under the base url first 794 addBundleBaseNamesFromClassLoader(bn, loader, set); 795 if (baseName.startsWith(ICUData.ICU_BASE_NAME)) { 796 String folder; 797 if (baseName.length() == ICUData.ICU_BASE_NAME.length()) { 798 folder = ""; 799 } else if (baseName.charAt(ICUData.ICU_BASE_NAME.length()) == '/') { 800 folder = baseName.substring(ICUData.ICU_BASE_NAME.length() + 1); 801 } else { 802 folder = null; 803 } 804 if (folder != null) { 805 ICUBinary.addBaseNamesInFileFolder(folder, ".res", set); 806 } 807 } 808 set.remove(ICU_RESOURCE_INDEX); // "res_index" 809 // HACK: TODO: Figure out how we can distinguish locale data from other data items. 810 Iterator<String> iter = set.iterator(); 811 while (iter.hasNext()) { 812 String name = iter.next(); 813 if ((name.length() == 1 || name.length() > 3) && name.indexOf('_') < 0) { 814 // Does not look like a locale ID. 815 iter.remove(); 816 } 817 } 818 } 819 // look for prebuilt full locale names list next 820 if (set.isEmpty()) { 821 if (DEBUG) System.out.println("unable to enumerate data files in " + baseName); 822 addLocaleIDsFromListFile(bn, loader, set); 823 } 824 if (set.isEmpty()) { 825 // Use locale name set as the last resort fallback 826 addLocaleIDsFromIndexBundle(baseName, loader, set); 827 } 828 // We need to have the root locale in the set, but not as "root". 829 set.remove("root"); 830 set.add(ULocale.ROOT.toString()); // "" 831 return Collections.unmodifiableSet(set); 832 } 833 createLocaleNameSet(String baseName, ClassLoader loader)834 private static Set<String> createLocaleNameSet(String baseName, ClassLoader loader) { 835 HashSet<String> set = new HashSet<>(); 836 addLocaleIDsFromIndexBundle(baseName, loader, set); 837 return Collections.unmodifiableSet(set); 838 } 839 840 /** 841 * Holds the prefix, and lazily creates the Locale[] list or the locale name 842 * Set as needed. 843 */ 844 private static final class AvailEntry { 845 private String prefix; 846 private ClassLoader loader; 847 private volatile EnumMap<ULocale.AvailableType, ULocale[]> ulocales; 848 private volatile Locale[] locales; 849 private volatile Set<String> nameSet; 850 private volatile Set<String> fullNameSet; 851 AvailEntry(String prefix, ClassLoader loader)852 AvailEntry(String prefix, ClassLoader loader) { 853 this.prefix = prefix; 854 this.loader = loader; 855 } 856 getULocaleList(ULocale.AvailableType type)857 ULocale[] getULocaleList(ULocale.AvailableType type) { 858 // Direct data is available for DEFAULT and ONLY_LEGACY_ALIASES 859 assert type != ULocale.AvailableType.WITH_LEGACY_ALIASES; 860 if (ulocales == null) { 861 synchronized(this) { 862 if (ulocales == null) { 863 ulocales = createULocaleList(prefix, loader); 864 } 865 } 866 } 867 return ulocales.get(type); 868 } getLocaleList(ULocale.AvailableType type)869 Locale[] getLocaleList(ULocale.AvailableType type) { 870 if (locales == null) { 871 getULocaleList(type); 872 synchronized(this) { 873 if (locales == null) { 874 locales = ICUResourceBundle.getLocaleList(ulocales.get(type)); 875 } 876 } 877 } 878 return locales; 879 } getLocaleNameSet()880 Set<String> getLocaleNameSet() { 881 if (nameSet == null) { 882 synchronized(this) { 883 if (nameSet == null) { 884 nameSet = createLocaleNameSet(prefix, loader); 885 } 886 } 887 } 888 return nameSet; 889 } getFullLocaleNameSet()890 Set<String> getFullLocaleNameSet() { 891 // When there's no prebuilt index, we iterate through the jar files 892 // and read the contents to build it. If many threads try to read the 893 // same jar at the same time, java thrashes. Synchronize here 894 // so that we can avoid this problem. We don't synchronize on the 895 // other methods since they don't do this. 896 // 897 // This is the common entry point for access into the code that walks 898 // through the resources, and is cached. So it's a good place to lock 899 // access. Locking in the URLHandler doesn't give us a common object 900 // to lock. 901 if (fullNameSet == null) { 902 synchronized(this) { 903 if (fullNameSet == null) { 904 fullNameSet = createFullLocaleNameSet(prefix, loader); 905 } 906 } 907 } 908 return fullNameSet; 909 } 910 } 911 912 913 /* 914 * Cache used for AvailableEntry 915 */ 916 private static CacheBase<String, AvailEntry, ClassLoader> GET_AVAILABLE_CACHE = 917 new SoftCache<String, AvailEntry, ClassLoader>() { 918 @Override 919 protected AvailEntry createInstance(String key, ClassLoader loader) { 920 return new AvailEntry(key, loader); 921 } 922 }; 923 924 /** 925 * Stores the locale information in a cache accessed by key (bundle prefix). 926 * The cached objects are AvailEntries. The cache is implemented by SoftCache 927 * so it can be GC'd. 928 */ getAvailEntry(String key, ClassLoader loader)929 private static AvailEntry getAvailEntry(String key, ClassLoader loader) { 930 return GET_AVAILABLE_CACHE.getInstance(key, loader); 931 } 932 findResourceWithFallback(String path, UResourceBundle actualBundle, UResourceBundle requested)933 private static final ICUResourceBundle findResourceWithFallback(String path, 934 UResourceBundle actualBundle, UResourceBundle requested) { 935 if (path.length() == 0) { 936 return null; 937 } 938 ICUResourceBundle base = (ICUResourceBundle) actualBundle; 939 // Collect existing and parsed key objects into an array of keys, 940 // rather than assembling and parsing paths. 941 int depth = base.getResDepth(); 942 int numPathKeys = countPathKeys(path); 943 assert numPathKeys > 0; 944 String[] keys = new String[depth + numPathKeys]; 945 getResPathKeys(path, numPathKeys, keys, depth); 946 return findResourceWithFallback(keys, depth, base, requested); 947 } 948 findResourceWithFallback( String[] keys, int depth, ICUResourceBundle base, UResourceBundle requested)949 private static final ICUResourceBundle findResourceWithFallback( 950 String[] keys, int depth, 951 ICUResourceBundle base, UResourceBundle requested) { 952 if (requested == null) { 953 requested = base; 954 } 955 956 for (;;) { // Iterate over the parent bundles. 957 for (;;) { // Iterate over the keys on the requested path, within a bundle. 958 String subKey = keys[depth++]; 959 ICUResourceBundle sub = (ICUResourceBundle) base.handleGet(subKey, null, requested); 960 if (sub == null) { 961 --depth; 962 break; 963 } 964 if (depth == keys.length) { 965 // We found it. 966 return sub; 967 } 968 base = sub; 969 } 970 // Try the parent bundle of the last-found resource. 971 ICUResourceBundle nextBase = base.getParent(); 972 if (nextBase == null) { 973 return null; 974 } 975 // If we followed an alias, then we may have switched bundle (locale) and key path. 976 // Set the lower parts of the path according to the last-found resource. 977 // This relies on a resource found via alias to have its original location information, 978 // rather than the location of the alias. 979 int baseDepth = base.getResDepth(); 980 if (depth != baseDepth) { 981 String[] newKeys = new String[baseDepth + (keys.length - depth)]; 982 System.arraycopy(keys, depth, newKeys, baseDepth, keys.length - depth); 983 keys = newKeys; 984 } 985 base.getResPathKeys(keys, baseDepth); 986 base = nextBase; 987 depth = 0; // getParent() returned a top level table resource. 988 } 989 } 990 991 /** 992 * Like findResourceWithFallback(...).getString() but with minimal creation of intermediate 993 * ICUResourceBundle objects. 994 */ findStringWithFallback(String path, UResourceBundle actualBundle, UResourceBundle requested)995 private static final String findStringWithFallback(String path, 996 UResourceBundle actualBundle, UResourceBundle requested) { 997 if (path.length() == 0) { 998 return null; 999 } 1000 if (!(actualBundle instanceof ICUResourceBundleImpl.ResourceContainer)) { 1001 return null; 1002 } 1003 if (requested == null) { 1004 requested = actualBundle; 1005 } 1006 1007 ICUResourceBundle base = (ICUResourceBundle) actualBundle; 1008 ICUResourceBundleReader reader = base.wholeBundle.reader; 1009 int res = RES_BOGUS; 1010 1011 // Collect existing and parsed key objects into an array of keys, 1012 // rather than assembling and parsing paths. 1013 int baseDepth = base.getResDepth(); 1014 int depth = baseDepth; 1015 int numPathKeys = countPathKeys(path); 1016 assert numPathKeys > 0; 1017 String[] keys = new String[depth + numPathKeys]; 1018 getResPathKeys(path, numPathKeys, keys, depth); 1019 1020 for (;;) { // Iterate over the parent bundles. 1021 for (;;) { // Iterate over the keys on the requested path, within a bundle. 1022 ICUResourceBundleReader.Container readerContainer; 1023 if (res == RES_BOGUS) { 1024 int type = base.getType(); 1025 if (type == TABLE || type == ARRAY) { 1026 readerContainer = ((ICUResourceBundleImpl.ResourceContainer)base).value; 1027 } else { 1028 break; 1029 } 1030 } else { 1031 int type = ICUResourceBundleReader.RES_GET_TYPE(res); 1032 if (ICUResourceBundleReader.URES_IS_TABLE(type)) { 1033 readerContainer = reader.getTable(res); 1034 } else if (ICUResourceBundleReader.URES_IS_ARRAY(type)) { 1035 readerContainer = reader.getArray(res); 1036 } else { 1037 res = RES_BOGUS; 1038 break; 1039 } 1040 } 1041 String subKey = keys[depth++]; 1042 res = readerContainer.getResource(reader, subKey); 1043 if (res == RES_BOGUS) { 1044 --depth; 1045 break; 1046 } 1047 ICUResourceBundle sub; 1048 if (ICUResourceBundleReader.RES_GET_TYPE(res) == ALIAS) { 1049 base.getResPathKeys(keys, baseDepth); 1050 sub = getAliasedResource(base, keys, depth, subKey, res, null, requested); 1051 } else { 1052 sub = null; 1053 } 1054 if (depth == keys.length) { 1055 // We found it. 1056 if (sub != null) { 1057 return sub.getString(); // string from alias handling 1058 } else { 1059 String s = reader.getString(res); 1060 if (s == null) { 1061 throw new UResourceTypeMismatchException(""); 1062 } 1063 return s; 1064 } 1065 } 1066 if (sub != null) { 1067 base = sub; 1068 reader = base.wholeBundle.reader; 1069 res = RES_BOGUS; 1070 // If we followed an alias, then we may have switched bundle (locale) and key path. 1071 // Reserve space for the lower parts of the path according to the last-found resource. 1072 // This relies on a resource found via alias to have its original location information, 1073 // rather than the location of the alias. 1074 baseDepth = base.getResDepth(); 1075 if (depth != baseDepth) { 1076 String[] newKeys = new String[baseDepth + (keys.length - depth)]; 1077 System.arraycopy(keys, depth, newKeys, baseDepth, keys.length - depth); 1078 keys = newKeys; 1079 depth = baseDepth; 1080 } 1081 } 1082 } 1083 // Try the parent bundle of the last-found resource. 1084 ICUResourceBundle nextBase = base.getParent(); 1085 if (nextBase == null) { 1086 return null; 1087 } 1088 // We probably have not yet set the lower parts of the key path. 1089 base.getResPathKeys(keys, baseDepth); 1090 base = nextBase; 1091 reader = base.wholeBundle.reader; 1092 depth = baseDepth = 0; // getParent() returned a top level table resource. 1093 } 1094 } 1095 getResDepth()1096 private int getResDepth() { 1097 return (container == null) ? 0 : container.getResDepth() + 1; 1098 } 1099 1100 /** 1101 * Fills some of the keys array with the keys on the path to this resource object. 1102 * Writes the top-level key into index 0 and increments from there. 1103 * 1104 * @param keys 1105 * @param depth must be {@link #getResDepth()} 1106 */ getResPathKeys(String[] keys, int depth)1107 private void getResPathKeys(String[] keys, int depth) { 1108 ICUResourceBundle b = this; 1109 while (depth > 0) { 1110 keys[--depth] = b.key; 1111 b = b.container; 1112 assert (depth == 0) == (b.container == null); 1113 } 1114 } 1115 countPathKeys(String path)1116 private static int countPathKeys(String path) { 1117 if (path.isEmpty()) { 1118 return 0; 1119 } 1120 int num = 1; 1121 for (int i = 0; i < path.length(); ++i) { 1122 if (path.charAt(i) == RES_PATH_SEP_CHAR) { 1123 ++num; 1124 } 1125 } 1126 return num; 1127 } 1128 1129 /** 1130 * Fills some of the keys array (from start) with the num keys from the path string. 1131 * 1132 * @param path path string 1133 * @param num must be {@link #countPathKeys(String)} 1134 * @param keys 1135 * @param start index where the first path key is stored 1136 */ getResPathKeys(String path, int num, String[] keys, int start)1137 private static void getResPathKeys(String path, int num, String[] keys, int start) { 1138 if (num == 0) { 1139 return; 1140 } 1141 if (num == 1) { 1142 keys[start] = path; 1143 return; 1144 } 1145 int i = 0; 1146 for (;;) { 1147 int j = path.indexOf(RES_PATH_SEP_CHAR, i); 1148 assert j >= i; 1149 keys[start++] = path.substring(i, j); 1150 if (num == 2) { 1151 assert path.indexOf(RES_PATH_SEP_CHAR, j + 1) < 0; 1152 keys[start] = path.substring(j + 1); 1153 break; 1154 } else { 1155 i = j + 1; 1156 --num; 1157 } 1158 } 1159 } 1160 1161 @Override 1162 public boolean equals(Object other) { 1163 if (this == other) { 1164 return true; 1165 } 1166 if (other instanceof ICUResourceBundle) { 1167 ICUResourceBundle o = (ICUResourceBundle) other; 1168 if (getBaseName().equals(o.getBaseName()) 1169 && getLocaleID().equals(o.getLocaleID())) { 1170 return true; 1171 } 1172 } 1173 return false; 1174 } 1175 1176 @Override 1177 public int hashCode() { 1178 assert false : "hashCode not designed"; 1179 return 42; 1180 } 1181 1182 public enum OpenType { // C++ uresbund.cpp: enum UResOpenType 1183 /** 1184 * Open a resource bundle for the locale; 1185 * if there is not even a base language bundle, then fall back to the default locale; 1186 * if there is no bundle for that either, then load the root bundle. 1187 * 1188 * <p>This is the default bundle loading behavior. 1189 */ 1190 LOCALE_DEFAULT_ROOT, 1191 // TODO: ICU ticket #11271 "consistent default locale across locale trees" 1192 // Add an option to look at the main locale tree for whether to 1193 // fall back to root directly (if the locale has main data) or 1194 // fall back to the default locale first (if the locale does not even have main data). 1195 /** 1196 * Open a resource bundle for the locale; 1197 * if there is not even a base language bundle, then load the root bundle; 1198 * never fall back to the default locale. 1199 * 1200 * <p>This is used for algorithms that have good pan-Unicode default behavior, 1201 * such as case mappings, collation, and segmentation (BreakIterator). 1202 */ 1203 LOCALE_ROOT, 1204 /** 1205 * Open a resource bundle for the locale; 1206 * if there is not even a base language bundle, then fail; 1207 * never fall back to the default locale nor to the root locale. 1208 * 1209 * <p>This is used when fallback to another language is not desired 1210 * and the root locale is not generally useful. 1211 * For example, {@link com.ibm.icu.util.LocaleData#setNoSubstitute(boolean)} 1212 * or currency display names for {@link com.ibm.icu.text.LocaleDisplayNames}. 1213 */ 1214 LOCALE_ONLY, 1215 /** 1216 * Open a resource bundle for the exact bundle name as requested; 1217 * no fallbacks, do not load parent bundles. 1218 * 1219 * <p>This is used for supplemental (non-locale) data. 1220 */ 1221 DIRECT 1222 }; 1223 1224 // This method is for super class's instantiateBundle method 1225 public static ICUResourceBundle getBundleInstance(String baseName, String localeID, 1226 ClassLoader root, boolean disableFallback) { 1227 return getBundleInstance(baseName, localeID, root, 1228 disableFallback ? OpenType.DIRECT : OpenType.LOCALE_DEFAULT_ROOT); 1229 } 1230 1231 public static ICUResourceBundle getBundleInstance( 1232 String baseName, ULocale locale, OpenType openType) { 1233 if (locale == null) { 1234 locale = ULocale.getDefault(); 1235 } 1236 return getBundleInstance(baseName, locale.getBaseName(), 1237 ICUResourceBundle.ICU_DATA_CLASS_LOADER, openType); 1238 } 1239 1240 public static ICUResourceBundle getBundleInstance(String baseName, String localeID, 1241 ClassLoader root, OpenType openType) { 1242 if (baseName == null) { 1243 baseName = ICUData.ICU_BASE_NAME; 1244 } 1245 localeID = ULocale.getBaseName(localeID); 1246 ICUResourceBundle b; 1247 if (openType == OpenType.LOCALE_DEFAULT_ROOT) { 1248 b = instantiateBundle(baseName, localeID, null, ULocale.getDefault().getBaseName(), 1249 root, openType); 1250 } else { 1251 b = instantiateBundle(baseName, localeID, null, null, root, openType); 1252 } 1253 if(b==null){ 1254 throw new MissingResourceException( 1255 "Could not find the bundle "+ baseName+"/"+ localeID+".res","",""); 1256 } 1257 return b; 1258 } 1259 1260 private static boolean localeIDStartsWithLangSubtag(String localeID, String lang) { 1261 return localeID.startsWith(lang) && 1262 (localeID.length() == lang.length() || localeID.charAt(lang.length()) == '_'); 1263 } 1264 1265 private static final Comparator<String[]> COMPARE_FIRST_ELEMENT = new Comparator<String[]>() { 1266 @Override 1267 public int compare(String[] pair1, String[] pair2) { 1268 return pair1[0].compareTo(pair2[0]); 1269 } 1270 }; 1271 1272 private static String getExplicitParent(String localeID) { 1273 return LocaleFallbackData.PARENT_LOCALE_TABLE.get(localeID); 1274 } 1275 1276 private static String getDefaultScript(String language, String region) { 1277 String localeID = language + "_" + region; 1278 String result = LocaleFallbackData.DEFAULT_SCRIPT_TABLE.get(localeID); 1279 if (result == null) { 1280 result = LocaleFallbackData.DEFAULT_SCRIPT_TABLE.get(language); 1281 } 1282 if (result == null) { 1283 result = "Latn"; 1284 } 1285 return result; 1286 } 1287 1288 private static String getParentLocaleID(String name, String origName, OpenType openType) { 1289 // early out if the locale ID has a variant code or ends with _ 1290 if (name.endsWith("_") || !ULocale.getVariant(name).isEmpty()) { 1291 int lastUnderbarPos = name.lastIndexOf('_'); 1292 if (lastUnderbarPos >= 0) { 1293 return name.substring(0, lastUnderbarPos); 1294 } else { 1295 return null; 1296 } 1297 } 1298 1299 // TODO: Is there a better way to break the locale ID up into its consituent parts? 1300 ULocale nameLocale = new ULocale(name); 1301 String language = nameLocale.getLanguage(); 1302 String script = nameLocale.getScript(); 1303 String region = nameLocale.getCountry(); 1304 1305 // if our open type is LOCALE_DEFAULT_ROOT, first look the locale ID up in the parent locale table; if that 1306 // table specifies a parent for it, return that (we don't do this for the other open types-- if we're not 1307 // falling back through the system default locale, we also want to do straight truncation fallback instead 1308 // of looking things up in the parent locale table-- see https://www.unicode.org/reports/tr35/tr35.html#Parent_Locales: 1309 // "Collation data, however, is an exception...") 1310 if (openType == OpenType.LOCALE_DEFAULT_ROOT) { 1311 String parentID = getExplicitParent(name); 1312 if (parentID != null) { 1313 return parentID.equals("root") ? null : parentID; 1314 } 1315 } 1316 1317 // if it's not in the parent locale table, figure out the fallback script algorithmically 1318 // (see CLDR-15265 for an explanation of the algorithm) 1319 if (!script.isEmpty() && !region.isEmpty()) { 1320 // if "name" has both script and region, is the script the default script? 1321 // - if so, remove it and keep the region 1322 // - if not, remove the region and keep the script 1323 if (getDefaultScript(language, region).equals(script)) { 1324 return language + "_" + region; 1325 } else { 1326 return language + "_" + script; 1327 } 1328 } else if (!region.isEmpty()) { 1329 // if "name" has region but not script, did the original locale ID specify a script? 1330 // - if yes, replace the region with the script from the original locale ID 1331 // - if no, replace the region with the default script for that language and region 1332 String origNameScript = ULocale.getScript(origName); 1333 if (!origNameScript.isEmpty()) { 1334 return language + "_" + origNameScript; 1335 } else { 1336 return language + "_" + getDefaultScript(language, region); 1337 } 1338 } else if (!script.isEmpty()) { 1339 // if "name" has script but not region (and our open type is LOCALE_DEFAULT_ROOT), is the script the 1340 // default script for the language? 1341 // - if so, remove it from the locale ID 1342 // - if not, return "root" (bypassing the system default locale ID) 1343 // (we don't do this for other open types for the same reason we don't look things up in the parent 1344 // locale table for other open types-- see the reference to UTS #35 above) 1345 if (openType != OpenType.LOCALE_DEFAULT_ROOT || getDefaultScript(language, null).equals(script)) { 1346 return language; 1347 } else { 1348 return /*"root"*/null; 1349 } 1350 } else { 1351 // if "name" just contains a language code, return null so the calling code falls back to "root" 1352 return null; 1353 } 1354 } 1355 instantiateBundle( final String baseName, final String localeID, final String origLocaleID, final String defaultID, final ClassLoader root, final OpenType openType)1356 private static ICUResourceBundle instantiateBundle( 1357 final String baseName, final String localeID, final String origLocaleID, final String defaultID, 1358 final ClassLoader root, final OpenType openType) { 1359 assert localeID.indexOf('@') < 0; 1360 assert defaultID == null || defaultID.indexOf('@') < 0; 1361 final String fullName = ICUResourceBundleReader.getFullName(baseName, localeID); 1362 char openTypeChar = (char)('0' + openType.ordinal()); 1363 String cacheKey = openType != OpenType.LOCALE_DEFAULT_ROOT ? 1364 fullName + '#' + openTypeChar : 1365 fullName + '#' + openTypeChar + '#' + defaultID; 1366 return BUNDLE_CACHE.getInstance(cacheKey, new Loader() { 1367 @Override 1368 public ICUResourceBundle load() { 1369 if(DEBUG) System.out.println("Creating "+fullName); 1370 // here we assume that java type resource bundle organization 1371 // is required then the base name contains '.' else 1372 // the resource organization is of ICU type 1373 // so clients can instantiate resources of the type 1374 // com.mycompany.data.MyLocaleElements_en.res and 1375 // com.mycompany.data.MyLocaleElements.res 1376 // 1377 final String rootLocale = (baseName.indexOf('.')==-1) ? "root" : ""; 1378 String localeName = localeID.isEmpty() ? rootLocale : localeID; 1379 ICUResourceBundle b = ICUResourceBundle.createBundle(baseName, localeName, root); 1380 1381 if(DEBUG)System.out.println("The bundle created is: "+b+" and openType="+openType+" and bundle.getNoFallback="+(b!=null && b.getNoFallback())); 1382 if (openType == OpenType.DIRECT || (b != null && b.getNoFallback())) { 1383 // no fallback because the caller said so or because the bundle says so 1384 // 1385 // TODO for b!=null: In C++, ures_openDirect() builds the parent chain 1386 // for its bundle unless its nofallback flag is set. 1387 // Otherwise we get test failures. 1388 // For example, item aliases are followed via ures_openDirect(), 1389 // and fail if the target bundle needs fallbacks but the chain is not set. 1390 // Figure out why Java does not build the parent chain 1391 // for a bundle that does not have nofallback. 1392 // Are the relevant test cases just disabled? 1393 // Do item aliases not get followed via "direct" loading? 1394 return b; 1395 } 1396 1397 // fallback to locale ID parent 1398 if(b == null){ 1399 String origLocaleName = (origLocaleID != null) ? origLocaleID : localeName; 1400 String fallbackLocaleID = getParentLocaleID(localeName, origLocaleName, openType); 1401 if (fallbackLocaleID != null) { 1402 b = instantiateBundle(baseName, fallbackLocaleID, origLocaleName, defaultID, root, openType); 1403 }else{ 1404 if(openType == OpenType.LOCALE_DEFAULT_ROOT && 1405 !localeIDStartsWithLangSubtag(defaultID, localeName)) { 1406 // Go to the default locale before root. 1407 b = instantiateBundle(baseName, defaultID, null, defaultID, root, openType); 1408 } else if(openType != OpenType.LOCALE_ONLY && !rootLocale.isEmpty()) { 1409 // Ultimately go to root. 1410 b = ICUResourceBundle.createBundle(baseName, rootLocale, root); 1411 } 1412 } 1413 }else{ 1414 UResourceBundle parent = null; 1415 localeName = b.getLocaleID(); 1416 int i = localeName.lastIndexOf('_'); 1417 1418 // TODO: C++ uresbund.cpp also checks for %%ParentIsRoot. Why not Java? 1419 String parentLocaleName = ((ICUResourceBundleImpl.ResourceTable)b).findString("%%Parent"); 1420 if (parentLocaleName != null) { 1421 parent = instantiateBundle(baseName, parentLocaleName, null, defaultID, root, openType); 1422 } else if (i != -1) { 1423 parent = instantiateBundle(baseName, localeName.substring(0, i), null, defaultID, root, openType); 1424 } else if (!localeName.equals(rootLocale)){ 1425 parent = instantiateBundle(baseName, rootLocale, null, defaultID, root, openType); 1426 } 1427 1428 if (!b.equals(parent)){ 1429 b.setParent(parent); 1430 } 1431 } 1432 return b; 1433 }}); 1434 } 1435 1436 ICUResourceBundle get(String aKey, HashMap<String, String> aliasesVisited, UResourceBundle requested) { 1437 ICUResourceBundle obj = (ICUResourceBundle)handleGet(aKey, aliasesVisited, requested); 1438 if (obj == null) { 1439 obj = getParent(); 1440 if (obj != null) { 1441 //call the get method to recursively fetch the resource 1442 obj = obj.get(aKey, aliasesVisited, requested); 1443 } 1444 if (obj == null) { 1445 String fullName = ICUResourceBundleReader.getFullName(getBaseName(), getLocaleID()); 1446 throw new MissingResourceException( 1447 "Can't find resource for bundle " + fullName + ", key " 1448 + aKey, this.getClass().getName(), aKey); 1449 } 1450 } 1451 return obj; 1452 } 1453 1454 /** Data member where the subclasses store the key. */ 1455 protected String key; 1456 1457 /** 1458 * A resource word value that means "no resource". 1459 * Note: 0xffffffff == -1 1460 * This has the same value as UResourceBundle.NONE, but they are semantically 1461 * different and should be used appropriately according to context: 1462 * NONE means "no type". 1463 * (The type of RES_BOGUS is RES_RESERVED=15 which was defined in ICU4C ures.h.) 1464 */ 1465 public static final int RES_BOGUS = 0xffffffff; 1466 //blic static final int RES_MAX_OFFSET = 0x0fffffff; 1467 1468 /** 1469 * Resource type constant for aliases; 1470 * internally stores a string which identifies the actual resource 1471 * storing the data (can be in a different resource bundle). 1472 * Resolved internally before delivering the actual resource through the API. 1473 */ 1474 public static final int ALIAS = 3; 1475 1476 /** Resource type constant for tables with 32-bit count, key offsets and values. */ 1477 public static final int TABLE32 = 4; 1478 1479 /** 1480 * Resource type constant for tables with 16-bit count, key offsets and values. 1481 * All values are STRING_V2 strings. 1482 */ 1483 public static final int TABLE16 = 5; 1484 1485 /** Resource type constant for 16-bit Unicode strings in formatVersion 2. */ 1486 public static final int STRING_V2 = 6; 1487 1488 /** 1489 * Resource type constant for arrays with 16-bit count and values. 1490 * All values are STRING_V2 strings. 1491 */ 1492 public static final int ARRAY16 = 9; 1493 1494 /* Resource type 15 is not defined but effectively used by RES_BOGUS=0xffffffff. */ 1495 1496 /** 1497 * Create a bundle using a reader. 1498 * @param baseName The name for the bundle. 1499 * @param localeID The locale identification. 1500 * @param root The ClassLoader object root. 1501 * @return the new bundle 1502 */ 1503 public static ICUResourceBundle createBundle(String baseName, String localeID, ClassLoader root) { 1504 ICUResourceBundleReader reader = ICUResourceBundleReader.getReader(baseName, localeID, root); 1505 if (reader == null) { 1506 // could not open the .res file 1507 return null; 1508 } 1509 return getBundle(reader, baseName, localeID, root); 1510 } 1511 1512 @Override 1513 protected String getLocaleID() { 1514 return wholeBundle.localeID; 1515 } 1516 1517 @Override 1518 protected String getBaseName() { 1519 return wholeBundle.baseName; 1520 } 1521 1522 @Override 1523 public ULocale getULocale() { 1524 return wholeBundle.ulocale; 1525 } 1526 1527 /** 1528 * Returns true if this is the root bundle, or an item in the root bundle. 1529 */ 1530 public boolean isRoot() { 1531 return wholeBundle.localeID.isEmpty() || wholeBundle.localeID.equals("root"); 1532 } 1533 1534 @Override 1535 public ICUResourceBundle getParent() { 1536 return (ICUResourceBundle) parent; 1537 } 1538 1539 @Override 1540 protected void setParent(ResourceBundle parent) { 1541 this.parent = parent; 1542 } 1543 1544 @Override 1545 public String getKey() { 1546 return key; 1547 } 1548 1549 /** 1550 * Get the noFallback flag specified in the loaded bundle. 1551 * @return The noFallback flag. 1552 */ 1553 private boolean getNoFallback() { 1554 return wholeBundle.reader.getNoFallback(); 1555 } 1556 1557 private static ICUResourceBundle getBundle(ICUResourceBundleReader reader, 1558 String baseName, String localeID, 1559 ClassLoader loader) { 1560 ICUResourceBundleImpl.ResourceTable rootTable; 1561 int rootRes = reader.getRootResource(); 1562 if(ICUResourceBundleReader.URES_IS_TABLE(ICUResourceBundleReader.RES_GET_TYPE(rootRes))) { 1563 WholeBundle wb = new WholeBundle(baseName, localeID, loader, reader); 1564 rootTable = new ICUResourceBundleImpl.ResourceTable(wb, rootRes); 1565 } else { 1566 throw new IllegalStateException("Invalid format error"); 1567 } 1568 String aliasString = rootTable.findString("%%ALIAS"); 1569 if(aliasString != null) { 1570 return (ICUResourceBundle)UResourceBundle.getBundleInstance(baseName, aliasString); 1571 } else { 1572 return rootTable; 1573 } 1574 } 1575 /** 1576 * Constructor for the root table of a bundle. 1577 */ 1578 protected ICUResourceBundle(WholeBundle wholeBundle) { 1579 this.wholeBundle = wholeBundle; 1580 } 1581 // constructor for inner classes 1582 protected ICUResourceBundle(ICUResourceBundle container, String key) { 1583 this.key = key; 1584 wholeBundle = container.wholeBundle; 1585 this.container = container; 1586 parent = container.parent; 1587 } 1588 1589 private static final char RES_PATH_SEP_CHAR = '/'; 1590 private static final String RES_PATH_SEP_STR = "/"; 1591 private static final String ICUDATA = "ICUDATA"; 1592 private static final char HYPHEN = '-'; 1593 private static final String LOCALE = "LOCALE"; 1594 1595 /** 1596 * Returns the resource object referred to from the alias _resource int's path string. 1597 * Throws MissingResourceException if not found. 1598 * 1599 * If the alias path does not contain a key path: 1600 * If keys != null then keys[:depth] is used. 1601 * Otherwise the base key path plus the key parameter is used. 1602 * 1603 * @param base A direct or indirect container of the alias. 1604 * @param keys The key path to the alias, or null. (const) 1605 * @param depth The length of the key path, if keys != null. 1606 * @param key The alias' own key within this current container, if keys == null. 1607 * @param _resource The alias resource int. 1608 * @param aliasesVisited Set of alias path strings already visited, for detecting loops. 1609 * We cannot change the type (e.g., to Set<String>) because it is used 1610 * in protected/@stable UResourceBundle methods. 1611 * @param requested The original resource object from which the lookup started, 1612 * which is the starting point for "/LOCALE/..." aliases. 1613 * @return the aliased resource object 1614 */ 1615 protected static ICUResourceBundle getAliasedResource( 1616 ICUResourceBundle base, String[] keys, int depth, 1617 String key, int _resource, 1618 HashMap<String, String> aliasesVisited, 1619 UResourceBundle requested) { 1620 WholeBundle wholeBundle = base.wholeBundle; 1621 ClassLoader loaderToUse = wholeBundle.loader; 1622 String rpath = wholeBundle.reader.getAlias(_resource); 1623 String baseName = wholeBundle.baseName; 1624 1625 // TODO: We need not build the baseKeyPath array if the rpath includes a keyPath 1626 // (except for an exception message string). 1627 // Try to avoid unnecessary work+allocation. 1628 int baseDepth = base.getResDepth(); 1629 String[] baseKeyPath = new String[baseDepth + 1]; 1630 base.getResPathKeys(baseKeyPath, baseDepth); 1631 baseKeyPath[baseDepth] = key; 1632 return getAliasedResource(rpath, loaderToUse, baseName, keys, depth, baseKeyPath, aliasesVisited, requested); 1633 } 1634 1635 protected static ICUResourceBundle getAliasedResource( 1636 String rpath, ClassLoader loaderToUse, String baseName, 1637 String[] keys, int depth, String[] baseKeyPath, 1638 HashMap<String, String> aliasesVisited, 1639 UResourceBundle requested) { 1640 String locale; 1641 String keyPath = null; 1642 String bundleName; 1643 if (aliasesVisited == null) { 1644 aliasesVisited = new HashMap<>(); 1645 } 1646 if (aliasesVisited.get(rpath) != null) { 1647 throw new IllegalArgumentException( 1648 "Circular references in the resource bundles"); 1649 } 1650 aliasesVisited.put(rpath, ""); 1651 if (rpath.indexOf(RES_PATH_SEP_CHAR) == 0) { 1652 int i = rpath.indexOf(RES_PATH_SEP_CHAR, 1); 1653 int j = rpath.indexOf(RES_PATH_SEP_CHAR, i + 1); 1654 bundleName = rpath.substring(1, i); 1655 if (j < 0) { 1656 locale = rpath.substring(i + 1); 1657 } else { 1658 locale = rpath.substring(i + 1, j); 1659 keyPath = rpath.substring(j + 1, rpath.length()); 1660 } 1661 //there is a path included 1662 if (bundleName.equals(ICUDATA)) { 1663 bundleName = ICUData.ICU_BASE_NAME; 1664 loaderToUse = ICU_DATA_CLASS_LOADER; 1665 }else if(bundleName.indexOf(ICUDATA)>-1){ 1666 int idx = bundleName.indexOf(HYPHEN); 1667 if(idx>-1){ 1668 bundleName = ICUData.ICU_BASE_NAME+RES_PATH_SEP_STR+bundleName.substring(idx+1,bundleName.length()); 1669 loaderToUse = ICU_DATA_CLASS_LOADER; 1670 } 1671 } 1672 } else { 1673 //no path start with locale 1674 int i = rpath.indexOf(RES_PATH_SEP_CHAR); 1675 if (i != -1) { 1676 locale = rpath.substring(0, i); 1677 keyPath = rpath.substring(i + 1); 1678 } else { 1679 locale = rpath; 1680 } 1681 bundleName = baseName; 1682 } 1683 ICUResourceBundle bundle = null; 1684 ICUResourceBundle sub = null; 1685 if(bundleName.equals(LOCALE)){ 1686 bundleName = baseName; 1687 keyPath = rpath.substring(LOCALE.length() + 2/* prepending and appending / */, rpath.length()); 1688 1689 // Get the top bundle of the requested bundle 1690 bundle = (ICUResourceBundle)requested; 1691 while (bundle.container != null) { 1692 bundle = bundle.container; 1693 } 1694 sub = ICUResourceBundle.findResourceWithFallback(keyPath, bundle, null); 1695 }else{ 1696 bundle = getBundleInstance(bundleName, locale, loaderToUse, false); 1697 1698 int numKeys; 1699 if (keyPath != null) { 1700 numKeys = countPathKeys(keyPath); 1701 if (numKeys > 0) { 1702 keys = new String[numKeys]; 1703 getResPathKeys(keyPath, numKeys, keys, 0); 1704 } 1705 } else if (keys != null) { 1706 numKeys = depth; 1707 } else { 1708 keys = baseKeyPath; 1709 numKeys = baseKeyPath.length; 1710 } 1711 if (numKeys > 0) { 1712 sub = bundle; 1713 for (int i = 0; sub != null && i < numKeys; ++i) { 1714 sub = sub.get(keys[i], aliasesVisited, requested); 1715 } 1716 } 1717 } 1718 if (sub == null) { 1719 throw new MissingResourceException(locale, baseName, baseKeyPath[baseKeyPath.length - 1]); 1720 } 1721 // TODO: If we know that sub is not cached, 1722 // then we should set its container and key to the alias' location, 1723 // so that it behaves as if its value had been copied into the alias location. 1724 // However, findResourceWithFallback() must reroute its bundle and key path 1725 // to where the alias data comes from. 1726 return sub; 1727 } 1728 1729 /** 1730 * @internal 1731 * @deprecated This API is ICU internal only. 1732 */ 1733 @Deprecated getTopLevelKeySet()1734 public final Set<String> getTopLevelKeySet() { 1735 return wholeBundle.topLevelKeys; 1736 } 1737 1738 /** 1739 * @internal 1740 * @deprecated This API is ICU internal only. 1741 */ 1742 @Deprecated setTopLevelKeySet(Set<String> keySet)1743 public final void setTopLevelKeySet(Set<String> keySet) { 1744 wholeBundle.topLevelKeys = keySet; 1745 } 1746 1747 // This is the worker function for the public getKeys(). 1748 // TODO: Now that UResourceBundle uses handleKeySet(), this function is obsolete. 1749 // It is also not inherited from ResourceBundle, and it is not implemented 1750 // by ResourceBundleWrapper despite its documentation requiring all subclasses to 1751 // implement it. 1752 // Consider deprecating UResourceBundle.handleGetKeys(), and consider making it always return null. 1753 @Override handleGetKeys()1754 protected Enumeration<String> handleGetKeys() { 1755 return Collections.enumeration(handleKeySet()); 1756 } 1757 1758 @Override isTopLevelResource()1759 protected boolean isTopLevelResource() { 1760 return container == null; 1761 } 1762 } 1763