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