001 /* ResourceBundle -- aids in loading resource bundles 002 Copyright (C) 1998, 1999, 2001, 2002, 2003, 2004, 2005, 2006 003 Free Software Foundation, Inc. 004 005 This file is part of GNU Classpath. 006 007 GNU Classpath is free software; you can redistribute it and/or modify 008 it under the terms of the GNU General Public License as published by 009 the Free Software Foundation; either version 2, or (at your option) 010 any later version. 011 012 GNU Classpath is distributed in the hope that it will be useful, but 013 WITHOUT ANY WARRANTY; without even the implied warranty of 014 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 General Public License for more details. 016 017 You should have received a copy of the GNU General Public License 018 along with GNU Classpath; see the file COPYING. If not, write to the 019 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 020 02110-1301 USA. 021 022 Linking this library statically or dynamically with other modules is 023 making a combined work based on this library. Thus, the terms and 024 conditions of the GNU General Public License cover the whole 025 combination. 026 027 As a special exception, the copyright holders of this library give you 028 permission to link this library with independent modules to produce an 029 executable, regardless of the license terms of these independent 030 modules, and to copy and distribute the resulting executable under 031 terms of your choice, provided that you also meet, for each linked 032 independent module, the terms and conditions of the license of that 033 module. An independent module is a module which is not derived from 034 or based on this library. If you modify this library, you may extend 035 this exception to your version of the library, but you are not 036 obligated to do so. If you do not wish to do so, delete this 037 exception statement from your version. */ 038 039 040 package java.util; 041 042 import gnu.classpath.VMStackWalker; 043 044 import gnu.java.lang.CPStringBuilder; 045 046 import java.io.IOException; 047 import java.io.InputStream; 048 049 /** 050 * A resource bundle contains locale-specific data. If you need localized 051 * data, you can load a resource bundle that matches the locale with 052 * <code>getBundle</code>. Now you can get your object by calling 053 * <code>getObject</code> or <code>getString</code> on that bundle. 054 * 055 * <p>When a bundle is demanded for a specific locale, the ResourceBundle 056 * is searched in following order (<i>def. language</i> stands for the 057 * two letter ISO language code of the default locale (see 058 * <code>Locale.getDefault()</code>). 059 * 060 <pre>baseName_<i>language code</i>_<i>country code</i>_<i>variant</i> 061 baseName_<i>language code</i>_<i>country code</i> 062 baseName_<i>language code</i> 063 baseName_<i>def. language</i>_<i>def. country</i>_<i>def. variant</i> 064 baseName_<i>def. language</i>_<i>def. country</i> 065 baseName_<i>def. language</i> 066 baseName</pre> 067 * 068 * <p>A bundle is backed up by less specific bundles (omitting variant, country 069 * or language). But it is not backed up by the default language locale. 070 * 071 * <p>If you provide a bundle for a given locale, say 072 * <code>Bundle_en_UK_POSIX</code>, you must also provide a bundle for 073 * all sub locales, ie. <code>Bundle_en_UK</code>, <code>Bundle_en</code>, and 074 * <code>Bundle</code>. 075 * 076 * <p>When a bundle is searched, we look first for a class with the given 077 * name, then for a file with <code>.properties</code> extension in the 078 * classpath. The name must be a fully qualified classname (with dots as 079 * path separators). 080 * 081 * <p>(Note: This implementation always backs up the class with a properties 082 * file if that is existing, but you shouldn't rely on this, if you want to 083 * be compatible to the standard JDK.) 084 * 085 * @author Jochen Hoenicke 086 * @author Eric Blake (ebb9@email.byu.edu) 087 * @see Locale 088 * @see ListResourceBundle 089 * @see PropertyResourceBundle 090 * @since 1.1 091 * @status updated to 1.4 092 */ 093 public abstract class ResourceBundle 094 { 095 /** 096 * Maximum size of our cache of <code>ResourceBundle</code>s keyed by 097 * {@link BundleKey} instances. 098 * 099 * @see BundleKey 100 */ 101 private static final int CACHE_SIZE = 100; 102 103 /** 104 * The parent bundle. This is consulted when you call getObject and there 105 * is no such resource in the current bundle. This field may be null. 106 */ 107 protected ResourceBundle parent; 108 109 /** 110 * The locale of this resource bundle. You can read this with 111 * <code>getLocale</code> and it is automatically set in 112 * <code>getBundle</code>. 113 */ 114 private Locale locale; 115 116 /** 117 * A VM-wide cache of resource bundles already fetched. 118 * <p> 119 * This {@link Map} is a Least Recently Used (LRU) cache, of the last 120 * {@link #CACHE_SIZE} accessed <code>ResourceBundle</code>s keyed by the 121 * tuple: default locale, resource-bundle name, resource-bundle locale, and 122 * classloader. 123 * 124 * @see BundleKey 125 */ 126 private static Map<BundleKey,Object> bundleCache = 127 new LinkedHashMap<BundleKey,Object>(CACHE_SIZE + 1, 0.75F, true) 128 { 129 public boolean removeEldestEntry(Map.Entry<BundleKey,Object> entry) 130 { 131 return size() > CACHE_SIZE; 132 } 133 }; 134 135 /** 136 * The constructor. It does nothing special. 137 */ 138 public ResourceBundle() 139 { 140 } 141 142 /** 143 * Get a String from this resource bundle. Since most localized Objects 144 * are Strings, this method provides a convenient way to get them without 145 * casting. 146 * 147 * @param key the name of the resource 148 * @throws MissingResourceException if the resource can't be found 149 * @throws NullPointerException if key is null 150 * @throws ClassCastException if resource is not a string 151 */ 152 public final String getString(String key) 153 { 154 return (String) getObject(key); 155 } 156 157 /** 158 * Get an array of Strings from this resource bundle. This method 159 * provides a convenient way to get it without casting. 160 * 161 * @param key the name of the resource 162 * @throws MissingResourceException if the resource can't be found 163 * @throws NullPointerException if key is null 164 * @throws ClassCastException if resource is not a string 165 */ 166 public final String[] getStringArray(String key) 167 { 168 return (String[]) getObject(key); 169 } 170 171 /** 172 * Get an object from this resource bundle. This will call 173 * <code>handleGetObject</code> for this resource and all of its parents, 174 * until it finds a non-null resource. 175 * 176 * @param key the name of the resource 177 * @throws MissingResourceException if the resource can't be found 178 * @throws NullPointerException if key is null 179 */ 180 public final Object getObject(String key) 181 { 182 for (ResourceBundle bundle = this; bundle != null; bundle = bundle.parent) 183 { 184 Object o = bundle.handleGetObject(key); 185 if (o != null) 186 return o; 187 } 188 189 String className = getClass().getName(); 190 throw new MissingResourceException("Key '" + key 191 + "'not found in Bundle: " 192 + className, className, key); 193 } 194 195 /** 196 * Return the actual locale of this bundle. You can use it after calling 197 * getBundle, to know if the bundle for the desired locale was loaded or 198 * if the fall back was used. 199 * 200 * @return the bundle's locale 201 */ 202 public Locale getLocale() 203 { 204 return locale; 205 } 206 207 /** 208 * Set the parent of this bundle. The parent is consulted when you call 209 * getObject and there is no such resource in the current bundle. 210 * 211 * @param parent the parent of this bundle 212 */ 213 protected void setParent(ResourceBundle parent) 214 { 215 this.parent = parent; 216 } 217 218 /** 219 * Get the appropriate ResourceBundle for the default locale. This is like 220 * calling <code>getBundle(baseName, Locale.getDefault(), 221 * getClass().getClassLoader()</code>, except that any security check of 222 * getClassLoader won't fail. 223 * 224 * @param baseName the name of the ResourceBundle 225 * @return the desired resource bundle 226 * @throws MissingResourceException if the resource bundle can't be found 227 * @throws NullPointerException if baseName is null 228 */ 229 public static ResourceBundle getBundle(String baseName) 230 { 231 ClassLoader cl = VMStackWalker.getCallingClassLoader(); 232 if (cl == null) 233 cl = ClassLoader.getSystemClassLoader(); 234 return getBundle(baseName, Locale.getDefault(), cl); 235 } 236 237 /** 238 * Get the appropriate ResourceBundle for the given locale. This is like 239 * calling <code>getBundle(baseName, locale, 240 * getClass().getClassLoader()</code>, except that any security check of 241 * getClassLoader won't fail. 242 * 243 * @param baseName the name of the ResourceBundle 244 * @param locale A locale 245 * @return the desired resource bundle 246 * @throws MissingResourceException if the resource bundle can't be found 247 * @throws NullPointerException if baseName or locale is null 248 */ 249 public static ResourceBundle getBundle(String baseName, Locale locale) 250 { 251 ClassLoader cl = VMStackWalker.getCallingClassLoader(); 252 if (cl == null) 253 cl = ClassLoader.getSystemClassLoader(); 254 return getBundle(baseName, locale, cl); 255 } 256 257 /** Cache key for the ResourceBundle cache. Resource bundles are keyed 258 by the combination of bundle name, locale, and class loader. */ 259 private static class BundleKey 260 { 261 Locale defaultLocale; 262 String baseName; 263 Locale locale; 264 ClassLoader classLoader; 265 int hashcode; 266 267 BundleKey() {} 268 269 BundleKey(Locale dl, String s, Locale l, ClassLoader cl) 270 { 271 set(dl, s, l, cl); 272 } 273 274 void set(Locale dl, String s, Locale l, ClassLoader cl) 275 { 276 defaultLocale = dl; 277 baseName = s; 278 locale = l; 279 classLoader = cl; 280 hashcode = defaultLocale.hashCode() ^ baseName.hashCode() 281 ^ locale.hashCode() ^ classLoader.hashCode(); 282 } 283 284 public int hashCode() 285 { 286 return hashcode; 287 } 288 289 public boolean equals(Object o) 290 { 291 if (! (o instanceof BundleKey)) 292 return false; 293 BundleKey key = (BundleKey) o; 294 return hashcode == key.hashcode 295 && defaultLocale.equals(key.defaultLocale) 296 && baseName.equals(key.baseName) 297 && locale.equals(key.locale) 298 && classLoader.equals(key.classLoader); 299 } 300 301 public String toString() 302 { 303 CPStringBuilder builder = new CPStringBuilder(getClass().getName()); 304 builder.append("[defaultLocale="); 305 builder.append(defaultLocale); 306 builder.append(",baseName="); 307 builder.append(baseName); 308 builder.append(",locale="); 309 builder.append(locale); 310 builder.append(",classLoader="); 311 builder.append(classLoader); 312 builder.append("]"); 313 return builder.toString(); 314 } 315 } 316 317 /** A cache lookup key. This avoids having to a new one for every 318 * getBundle() call. */ 319 private static final BundleKey lookupKey = new BundleKey(); 320 321 /** Singleton cache entry to represent previous failed lookups. */ 322 private static final Object nullEntry = new Object(); 323 324 /** 325 * Get the appropriate ResourceBundle for the given locale. The following 326 * strategy is used: 327 * 328 * <p>A sequence of candidate bundle names are generated, and tested in 329 * this order, where the suffix 1 means the string from the specified 330 * locale, and the suffix 2 means the string from the default locale:</p> 331 * 332 * <ul> 333 * <li>baseName + "_" + language1 + "_" + country1 + "_" + variant1</li> 334 * <li>baseName + "_" + language1 + "_" + country1</li> 335 * <li>baseName + "_" + language1</li> 336 * <li>baseName + "_" + language2 + "_" + country2 + "_" + variant2</li> 337 * <li>baseName + "_" + language2 + "_" + country2</li> 338 * <li>baseName + "_" + language2</li> 339 * <li>baseName</li> 340 * </ul> 341 * 342 * <p>In the sequence, entries with an empty string are ignored. Next, 343 * <code>getBundle</code> tries to instantiate the resource bundle:</p> 344 * 345 * <ul> 346 * <li>First, an attempt is made to load a class in the specified classloader 347 * which is a subclass of ResourceBundle, and which has a public constructor 348 * with no arguments, via reflection.</li> 349 * <li>Next, a search is made for a property resource file, by replacing 350 * '.' with '/' and appending ".properties", and using 351 * ClassLoader.getResource(). If a file is found, then a 352 * PropertyResourceBundle is created from the file's contents.</li> 353 * </ul> 354 * If no resource bundle was found, a MissingResourceException is thrown. 355 * 356 * <p>Next, the parent chain is implemented. The remaining candidate names 357 * in the above sequence are tested in a similar manner, and if any results 358 * in a resource bundle, it is assigned as the parent of the first bundle 359 * using the <code>setParent</code> method (unless the first bundle already 360 * has a parent).</p> 361 * 362 * <p>For example, suppose the following class and property files are 363 * provided: MyResources.class, MyResources_fr_CH.properties, 364 * MyResources_fr_CH.class, MyResources_fr.properties, 365 * MyResources_en.properties, and MyResources_es_ES.class. The contents of 366 * all files are valid (that is, public non-abstract subclasses of 367 * ResourceBundle with public nullary constructors for the ".class" files, 368 * syntactically correct ".properties" files). The default locale is 369 * Locale("en", "UK").</p> 370 * 371 * <p>Calling getBundle with the shown locale argument values instantiates 372 * resource bundles from the following sources:</p> 373 * 374 * <ul> 375 * <li>Locale("fr", "CH"): result MyResources_fr_CH.class, parent 376 * MyResources_fr.properties, parent MyResources.class</li> 377 * <li>Locale("fr", "FR"): result MyResources_fr.properties, parent 378 * MyResources.class</li> 379 * <li>Locale("de", "DE"): result MyResources_en.properties, parent 380 * MyResources.class</li> 381 * <li>Locale("en", "US"): result MyResources_en.properties, parent 382 * MyResources.class</li> 383 * <li>Locale("es", "ES"): result MyResources_es_ES.class, parent 384 * MyResources.class</li> 385 * </ul> 386 * 387 * <p>The file MyResources_fr_CH.properties is never used because it is hidden 388 * by MyResources_fr_CH.class.</p> 389 * 390 * @param baseName the name of the ResourceBundle 391 * @param locale A locale 392 * @param classLoader a ClassLoader 393 * @return the desired resource bundle 394 * @throws MissingResourceException if the resource bundle can't be found 395 * @throws NullPointerException if any argument is null 396 * @since 1.2 397 */ 398 // This method is synchronized so that the cache is properly 399 // handled. 400 public static synchronized ResourceBundle getBundle 401 (String baseName, Locale locale, ClassLoader classLoader) 402 { 403 Locale defaultLocale = Locale.getDefault(); 404 // This will throw NullPointerException if any arguments are null. 405 lookupKey.set(defaultLocale, baseName, locale, classLoader); 406 Object obj = bundleCache.get(lookupKey); 407 if (obj instanceof ResourceBundle) 408 return (ResourceBundle) obj; 409 410 if (obj == nullEntry) 411 throw new MissingResourceException("Bundle " + baseName 412 + " not found for locale " + locale 413 + " by classloader " + classLoader, 414 baseName, ""); 415 // First, look for a bundle for the specified locale. We don't want 416 // the base bundle this time. 417 boolean wantBase = locale.equals(defaultLocale); 418 ResourceBundle bundle = tryBundle(baseName, locale, classLoader, wantBase); 419 // Try the default locale if neccessary. 420 if (bundle == null && ! wantBase) 421 bundle = tryBundle(baseName, defaultLocale, classLoader, true); 422 423 BundleKey key = new BundleKey(defaultLocale, baseName, locale, classLoader); 424 if (bundle == null) 425 { 426 // Cache the fact that this lookup has previously failed. 427 bundleCache.put(key, nullEntry); 428 throw new MissingResourceException("Bundle " + baseName 429 + " not found for locale " + locale 430 + " by classloader " + classLoader, 431 baseName, ""); 432 } 433 // Cache the result and return it. 434 bundleCache.put(key, bundle); 435 return bundle; 436 } 437 438 /** 439 * Override this method to provide the resource for a keys. This gets 440 * called by <code>getObject</code>. If you don't have a resource 441 * for the given key, you should return null instead throwing a 442 * MissingResourceException. You don't have to ask the parent, getObject() 443 * already does this; nor should you throw a MissingResourceException. 444 * 445 * @param key the key of the resource 446 * @return the resource for the key, or null if not in bundle 447 * @throws NullPointerException if key is null 448 */ 449 protected abstract Object handleGetObject(String key); 450 451 /** 452 * This method should return all keys for which a resource exists; you 453 * should include the enumeration of any parent's keys, after filtering out 454 * duplicates. 455 * 456 * @return an enumeration of the keys 457 */ 458 public abstract Enumeration<String> getKeys(); 459 460 /** 461 * Tries to load a class or a property file with the specified name. 462 * 463 * @param localizedName the name 464 * @param classloader the classloader 465 * @return the resource bundle if it was loaded, otherwise the backup 466 */ 467 private static ResourceBundle tryBundle(String localizedName, 468 ClassLoader classloader) 469 { 470 ResourceBundle bundle = null; 471 try 472 { 473 Class<?> rbClass; 474 if (classloader == null) 475 rbClass = Class.forName(localizedName); 476 else 477 rbClass = classloader.loadClass(localizedName); 478 // Note that we do the check up front instead of catching 479 // ClassCastException. The reason for this is that some crazy 480 // programs (Eclipse) have classes that do not extend 481 // ResourceBundle but that have the same name as a property 482 // bundle; in fact Eclipse relies on ResourceBundle not 483 // instantiating these classes. 484 if (ResourceBundle.class.isAssignableFrom(rbClass)) 485 bundle = (ResourceBundle) rbClass.newInstance(); 486 } 487 catch (Exception ex) {} 488 489 if (bundle == null) 490 { 491 try 492 { 493 InputStream is; 494 String resourceName 495 = localizedName.replace('.', '/') + ".properties"; 496 if (classloader == null) 497 is = ClassLoader.getSystemResourceAsStream(resourceName); 498 else 499 is = classloader.getResourceAsStream(resourceName); 500 if (is != null) 501 bundle = new PropertyResourceBundle(is); 502 } 503 catch (IOException ex) 504 { 505 MissingResourceException mre = new MissingResourceException 506 ("Failed to load bundle: " + localizedName, localizedName, ""); 507 mre.initCause(ex); 508 throw mre; 509 } 510 } 511 512 return bundle; 513 } 514 515 /** 516 * Tries to load the bundle for a given locale, also loads the backup 517 * locales with the same language. 518 * 519 * @param baseName the raw bundle name, without locale qualifiers 520 * @param locale the locale 521 * @param classLoader the classloader 522 * @param wantBase whether a resource bundle made only from the base name 523 * (with no locale information attached) should be returned. 524 * @return the resource bundle if it was loaded, otherwise the backup 525 */ 526 private static ResourceBundle tryBundle(String baseName, Locale locale, 527 ClassLoader classLoader, 528 boolean wantBase) 529 { 530 String language = locale.getLanguage(); 531 String country = locale.getCountry(); 532 String variant = locale.getVariant(); 533 534 int baseLen = baseName.length(); 535 536 // Build up a CPStringBuilder containing the complete bundle name, fully 537 // qualified by locale. 538 CPStringBuilder sb = new CPStringBuilder(baseLen + variant.length() + 7); 539 540 sb.append(baseName); 541 542 if (language.length() > 0) 543 { 544 sb.append('_'); 545 sb.append(language); 546 547 if (country.length() > 0) 548 { 549 sb.append('_'); 550 sb.append(country); 551 552 if (variant.length() > 0) 553 { 554 sb.append('_'); 555 sb.append(variant); 556 } 557 } 558 } 559 560 // Now try to load bundles, starting with the most specialized name. 561 // Build up the parent chain as we go. 562 String bundleName = sb.toString(); 563 ResourceBundle first = null; // The most specialized bundle. 564 ResourceBundle last = null; // The least specialized bundle. 565 566 while (true) 567 { 568 ResourceBundle foundBundle = tryBundle(bundleName, classLoader); 569 if (foundBundle != null) 570 { 571 if (first == null) 572 first = foundBundle; 573 if (last != null) 574 last.parent = foundBundle; 575 foundBundle.locale = locale; 576 last = foundBundle; 577 } 578 int idx = bundleName.lastIndexOf('_'); 579 // Try the non-localized base name only if we already have a 580 // localized child bundle, or wantBase is true. 581 if (idx > baseLen || (idx == baseLen && (first != null || wantBase))) 582 bundleName = bundleName.substring(0, idx); 583 else 584 break; 585 } 586 587 return first; 588 } 589 590 /** 591 * Remove all resources from the cache that were loaded 592 * using the class loader of the calling class. 593 * 594 * @since 1.6 595 */ 596 public static final void clearCache() 597 { 598 clearCache(VMStackWalker.getCallingClassLoader()); 599 } 600 601 /** 602 * Remove all resources from the cache that were loaded 603 * using the specified class loader. 604 * 605 * @param loader the loader used for the bundles that will be removed. 606 * @throws NullPointerException if {@code loader} is {@code null}. 607 * @since 1.6 608 */ 609 public static final void clearCache(ClassLoader loader) 610 { 611 if (loader == null) 612 throw new NullPointerException("The loader can not be null."); 613 synchronized (ResourceBundle.class) 614 { 615 Iterator<BundleKey> iter = bundleCache.keySet().iterator(); 616 while (iter.hasNext()) 617 { 618 BundleKey key = iter.next(); 619 if (key.classLoader == loader) 620 iter.remove(); 621 } 622 } 623 } 624 625 }