ResourceBundle.java 19.3 KB
Newer Older
1
/* ResourceBundle -- aids in loading resource bundles
2
   Copyright (C) 1998, 1999, 2001, 2002, 2003, 2004, 2005
3
   Free Software Foundation, Inc.
Tom Tromey committed
4

Tom Tromey committed
5
This file is part of GNU Classpath.
Tom Tromey committed
6

Tom Tromey committed
7 8 9 10
GNU Classpath is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.
Tom Tromey committed
11

Tom Tromey committed
12 13 14 15
GNU Classpath is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.
Tom Tromey committed
16

Tom Tromey committed
17 18 19 20
You should have received a copy of the GNU General Public License
along with GNU Classpath; see the file COPYING.  If not, write to the
Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
02111-1307 USA.
21

22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
Linking this library statically or dynamically with other modules is
making a combined work based on this library.  Thus, the terms and
conditions of the GNU General Public License cover the whole
combination.

As a special exception, the copyright holders of this library give you
permission to link this library with independent modules to produce an
executable, regardless of the license terms of these independent
modules, and to copy and distribute the resulting executable under
terms of your choice, provided that you also meet, for each linked
independent module, the terms and conditions of the license of that
module.  An independent module is a module which is not derived from
or based on this library.  If you modify this library, you may extend
this exception to your version of the library, but you are not
obligated to do so.  If you do not wish to do so, delete this
exception statement from your version. */
Tom Tromey committed
38 39


Tom Tromey committed
40
package java.util;
41

Michael Koch committed
42 43
import java.io.IOException;
import java.io.InputStream;
Tom Tromey committed
44 45

/**
46 47 48 49 50 51
 * A resource bundle contains locale-specific data. If you need localized
 * data, you can load a resource bundle that matches the locale with
 * <code>getBundle</code>. Now you can get your object by calling
 * <code>getObject</code> or <code>getString</code> on that bundle.
 *
 * <p>When a bundle is demanded for a specific locale, the ResourceBundle
52
 * is searched in following order (<i>def. language</i> stands for the
53
 * two letter ISO language code of the default locale (see
Tom Tromey committed
54 55
 * <code>Locale.getDefault()</code>).
 *
56 57 58 59 60 61 62 63
<pre>baseName_<i>language code</i>_<i>country code</i>_<i>variant</i>
baseName_<i>language code</i>_<i>country code</i>
baseName_<i>language code</i>
baseName_<i>def. language</i>_<i>def. country</i>_<i>def. variant</i>
baseName_<i>def. language</i>_<i>def. country</i>
baseName_<i>def. language</i>
baseName</pre>
 *
64
 * <p>A bundle is backed up by less specific bundles (omitting variant, country
65 66 67
 * or language). But it is not backed up by the default language locale.
 *
 * <p>If you provide a bundle for a given locale, say
Tom Tromey committed
68 69 70 71
 * <code>Bundle_en_UK_POSIX</code>, you must also provide a bundle for
 * all sub locales, ie. <code>Bundle_en_UK</code>, <code>Bundle_en</code>, and
 * <code>Bundle</code>.
 *
72 73 74 75 76 77 78 79 80 81 82
 * <p>When a bundle is searched, we look first for a class with the given
 * name, then for a file with <code>.properties</code> extension in the
 * classpath. The name must be a fully qualified classname (with dots as
 * path separators).
 *
 * <p>(Note: This implementation always backs up the class with a properties
 * file if that is existing, but you shouldn't rely on this, if you want to
 * be compatible to the standard JDK.)
 *
 * @author Jochen Hoenicke
 * @author Eric Blake (ebb9@email.byu.edu)
Tom Tromey committed
83
 * @see Locale
84
 * @see ListResourceBundle
Tom Tromey committed
85
 * @see PropertyResourceBundle
86 87 88
 * @since 1.1
 * @status updated to 1.4
 */
Tom Tromey committed
89 90
public abstract class ResourceBundle
{
Tom Tromey committed
91
  /**
92 93
   * The parent bundle. This is consulted when you call getObject and there
   * is no such resource in the current bundle. This field may be null.
Tom Tromey committed
94
   */
Tom Tromey committed
95 96
  protected ResourceBundle parent;

Tom Tromey committed
97
  /**
98
   * The locale of this resource bundle. You can read this with
Tom Tromey committed
99
   * <code>getLocale</code> and it is automatically set in
100
   * <code>getBundle</code>.
Tom Tromey committed
101 102 103
   */
  private Locale locale;

Tom Tromey committed
104
  private static native ClassLoader getCallingClassLoader();
105 106

  /**
107
   * The resource bundle cache.
108
   */
109
  private static Map bundleCache;
110 111 112 113 114 115

  /**
   * The last default Locale we saw. If this ever changes then we have to
   * reset our caches.
   */
  private static Locale lastDefaultLocale;
116 117 118 119 120 121 122 123 124

  /**
   * The `empty' locale is created once in order to optimize
   * tryBundle().
   */
  private static final Locale emptyLocale = new Locale("");

  /**
   * The constructor. It does nothing special.
Tom Tromey committed
125 126
   */
  public ResourceBundle()
Bryce McKinlay committed
127 128
  {
  }
Tom Tromey committed
129

Tom Tromey committed
130
  /**
131 132 133 134 135 136 137 138
   * Get a String from this resource bundle. Since most localized Objects
   * are Strings, this method provides a convenient way to get them without
   * casting.
   *
   * @param key the name of the resource
   * @throws MissingResourceException if the resource can't be found
   * @throws NullPointerException if key is null
   * @throws ClassCastException if resource is not a string
Tom Tromey committed
139
   */
140
  public final String getString(String key)
Tom Tromey committed
141 142 143
  {
    return (String) getObject(key);
  }
Tom Tromey committed
144

Tom Tromey committed
145
  /**
146
   * Get an array of Strings from this resource bundle. This method
Tom Tromey committed
147
   * provides a convenient way to get it without casting.
148 149 150 151 152
   *
   * @param key the name of the resource
   * @throws MissingResourceException if the resource can't be found
   * @throws NullPointerException if key is null
   * @throws ClassCastException if resource is not a string
Tom Tromey committed
153 154 155 156 157
   */
  public final String[] getStringArray(String key)
  {
    return (String[]) getObject(key);
  }
Tom Tromey committed
158

Tom Tromey committed
159
  /**
160 161 162 163 164 165 166
   * Get an object from this resource bundle. This will call
   * <code>handleGetObject</code> for this resource and all of its parents,
   * until it finds a non-null resource.
   *
   * @param key the name of the resource
   * @throws MissingResourceException if the resource can't be found
   * @throws NullPointerException if key is null
Tom Tromey committed
167
   */
168
  public final Object getObject(String key)
Tom Tromey committed
169 170
  {
    for (ResourceBundle bundle = this; bundle != null; bundle = bundle.parent)
171 172 173 174 175
      {
        Object o = bundle.handleGetObject(key);
        if (o != null)
          return o;
      }
176 177 178 179 180

    String className = getClass().getName();
    throw new MissingResourceException("Key '" + key
				       + "'not found in Bundle: "
				       + className, className, key);
Tom Tromey committed
181
  }
Tom Tromey committed
182

Tom Tromey committed
183
  /**
184 185 186 187 188
   * Return the actual locale of this bundle. You can use it after calling
   * getBundle, to know if the bundle for the desired locale was loaded or
   * if the fall back was used.
   *
   * @return the bundle's locale
189
   */
190
  public Locale getLocale()
Tom Tromey committed
191
  {
192
    return locale;
Tom Tromey committed
193
  }
Tom Tromey committed
194

Tom Tromey committed
195
  /**
196 197 198 199
   * Set the parent of this bundle. The parent is consulted when you call
   * getObject and there is no such resource in the current bundle.
   *
   * @param parent the parent of this bundle
Tom Tromey committed
200
   */
201
  protected void setParent(ResourceBundle parent)
Tom Tromey committed
202
  {
203
    this.parent = parent;
Tom Tromey committed
204
  }
Tom Tromey committed
205

Tom Tromey committed
206
  /**
207 208 209 210 211 212 213 214 215
   * Get the appropriate ResourceBundle for the default locale. This is like
   * calling <code>getBundle(baseName, Locale.getDefault(),
   * getClass().getClassLoader()</code>, except that any security check of
   * getClassLoader won't fail.
   *
   * @param baseName the name of the ResourceBundle
   * @return the desired resource bundle
   * @throws MissingResourceException if the resource bundle can't be found
   * @throws NullPointerException if baseName is null
Tom Tromey committed
216
   */
217
  public static ResourceBundle getBundle(String baseName)
Tom Tromey committed
218
  {
219 220 221 222
    ClassLoader cl = getCallingClassLoader();
    if (cl == null)
      cl = ClassLoader.getSystemClassLoader();
    return getBundle(baseName, Locale.getDefault(), cl);
Tom Tromey committed
223
  }
Tom Tromey committed
224

Tom Tromey committed
225
  /**
226 227 228 229
   * Get the appropriate ResourceBundle for the given locale. This is like
   * calling <code>getBundle(baseName, locale,
   * getClass().getClassLoader()</code>, except that any security check of
   * getClassLoader won't fail.
Tom Tromey committed
230
   *
231 232 233 234 235
   * @param baseName the name of the ResourceBundle
   * @param locale A locale
   * @return the desired resource bundle
   * @throws MissingResourceException if the resource bundle can't be found
   * @throws NullPointerException if baseName or locale is null
Tom Tromey committed
236
   */
237
  public static ResourceBundle getBundle(String baseName, Locale locale)
238
  {
239 240 241 242 243 244 245 246
    ClassLoader cl = getCallingClassLoader();
    if (cl == null)
      cl = ClassLoader.getSystemClassLoader();
    return getBundle(baseName, locale, cl);
  }

  /** Cache key for the ResourceBundle cache.  Resource bundles are keyed
      by the combination of bundle name, locale, and class loader. */
247
  private static class BundleKey
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283
  {
    String baseName;
    Locale locale;
    ClassLoader classLoader;
    int hashcode;

    BundleKey() {}

    BundleKey(String s, Locale l, ClassLoader cl)
    {
      set(s, l, cl);
    }
    
    void set(String s, Locale l, ClassLoader cl)
    {
      baseName = s;
      locale = l;
      classLoader = cl;
      hashcode = baseName.hashCode() ^ locale.hashCode() ^
        classLoader.hashCode();
    }
    
    public int hashCode()
    {
      return hashcode;
    }
    
    public boolean equals(Object o)
    {
      if (! (o instanceof BundleKey))
        return false;
      BundleKey key = (BundleKey) o;
      return hashcode == key.hashcode &&
	baseName.equals(key.baseName) &&
        locale.equals(key.locale) &&
	classLoader.equals(key.classLoader);
284
    }    
285
  }
286 287 288 289 290 291 292
  
  /** A cache lookup key. This avoids having to a new one for every
   *  getBundle() call. */
  private static BundleKey lookupKey = new BundleKey();
  
  /** Singleton cache entry to represent previous failed lookups. */
  private static Object nullEntry = new Object();
293

Tom Tromey committed
294
  /**
295 296 297 298 299
   * Get the appropriate ResourceBundle for the given locale. The following
   * strategy is used:
   *
   * <p>A sequence of candidate bundle names are generated, and tested in
   * this order, where the suffix 1 means the string from the specified
300 301 302
   * locale, and the suffix 2 means the string from the default locale:</p>
   *
   * <ul>
303 304 305 306 307
   * <li>baseName + "_" + language1 + "_" + country1 + "_" + variant1</li>
   * <li>baseName + "_" + language1 + "_" + country1</li>
   * <li>baseName + "_" + language1</li>
   * <li>baseName + "_" + language2 + "_" + country2 + "_" + variant2</li>
   * <li>baseName + "_" + language2 + "_" + country2</li>
308
   * <li>baseName + "_" + language2</li>
309 310 311 312
   * <li>baseName</li>
   * </ul>
   *
   * <p>In the sequence, entries with an empty string are ignored. Next,
313 314 315
   * <code>getBundle</code> tries to instantiate the resource bundle:</p>
   *
   * <ul>
316 317 318 319 320 321 322 323 324 325 326 327 328 329
   * <li>First, an attempt is made to load a class in the specified classloader
   * which is a subclass of ResourceBundle, and which has a public constructor
   * with no arguments, via reflection.</li>
   * <li>Next, a search is made for a property resource file, by replacing
   * '.' with '/' and appending ".properties", and using
   * ClassLoader.getResource(). If a file is found, then a
   * PropertyResourceBundle is created from the file's contents.</li>
   * </ul>
   * If no resource bundle was found, a MissingResourceException is thrown.
   *
   * <p>Next, the parent chain is implemented. The remaining candidate names
   * in the above sequence are tested in a similar manner, and if any results
   * in a resource bundle, it is assigned as the parent of the first bundle
   * using the <code>setParent</code> method (unless the first bundle already
330
   * has a parent).</p>
331 332 333 334 335 336 337 338
   *
   * <p>For example, suppose the following class and property files are
   * provided: MyResources.class, MyResources_fr_CH.properties,
   * MyResources_fr_CH.class, MyResources_fr.properties,
   * MyResources_en.properties, and MyResources_es_ES.class. The contents of
   * all files are valid (that is, public non-abstract subclasses of
   * ResourceBundle with public nullary constructors for the ".class" files,
   * syntactically correct ".properties" files). The default locale is
339
   * Locale("en", "UK").</p>
340 341
   *
   * <p>Calling getBundle with the shown locale argument values instantiates
342 343 344
   * resource bundles from the following sources:</p>
   *
   * <ul>
345 346 347 348 349 350 351 352 353 354 355
   * <li>Locale("fr", "CH"): result MyResources_fr_CH.class, parent
   *   MyResources_fr.properties, parent MyResources.class</li>
   * <li>Locale("fr", "FR"): result MyResources_fr.properties, parent
   *   MyResources.class</li>
   * <li>Locale("de", "DE"): result MyResources_en.properties, parent
   *   MyResources.class</li>
   * <li>Locale("en", "US"): result MyResources_en.properties, parent
   *   MyResources.class</li>
   * <li>Locale("es", "ES"): result MyResources_es_ES.class, parent
   *   MyResources.class</li>
   * </ul>
356 357 358
   * 
   * <p>The file MyResources_fr_CH.properties is never used because it is hidden
   * by MyResources_fr_CH.class.</p>
359 360 361 362
   *
   * @param baseName the name of the ResourceBundle
   * @param locale A locale
   * @param classloader a ClassLoader
Tom Tromey committed
363
   * @return the desired resource bundle
364 365 366
   * @throws MissingResourceException if the resource bundle can't be found
   * @throws NullPointerException if any argument is null
   * @since 1.2
Tom Tromey committed
367 368 369
   */
  // This method is synchronized so that the cache is properly
  // handled.
370
  public static synchronized ResourceBundle getBundle
371
    (String baseName, Locale locale, ClassLoader classLoader)
Tom Tromey committed
372
  {
373 374
    // If the default locale changed since the last time we were called,
    // all cache entries are invalidated.
375 376 377
    Locale defaultLocale = Locale.getDefault();
    if (defaultLocale != lastDefaultLocale)
      {
378
	bundleCache = new HashMap();
379 380
	lastDefaultLocale = defaultLocale;
      }
381

382 383 384 385 386
    // This will throw NullPointerException if any arguments are null.
    lookupKey.set(baseName, locale, classLoader);
    
    Object obj = bundleCache.get(lookupKey);
    ResourceBundle rb = null;
387
    
388
    if (obj instanceof ResourceBundle)
Tom Tromey committed
389
      {
390
        return (ResourceBundle) obj;
Tom Tromey committed
391
      }
392
    else if (obj == nullEntry)
Tom Tromey committed
393
      {
394
        // Lookup has failed previously. Fall through.
Tom Tromey committed
395
      }
396
    else
397
      {
398 399 400 401 402 403 404 405 406 407
	// First, look for a bundle for the specified locale. We don't want
	// the base bundle this time.
	boolean wantBase = locale.equals(defaultLocale);
	ResourceBundle bundle = tryBundle(baseName, locale, classLoader, 
					  wantBase);

        // Try the default locale if neccessary.
	if (bundle == null && !locale.equals(defaultLocale))
	  bundle = tryBundle(baseName, defaultLocale, classLoader, true);

408
	BundleKey key = new BundleKey(baseName, locale, classLoader);
409 410 411 412 413
        if (bundle == null)
	  {
	    // Cache the fact that this lookup has previously failed.
	    bundleCache.put(key, nullEntry);
	  }
414
	else
415 416 417 418 419
	  {
            // Cache the result and return it.
	    bundleCache.put(key, bundle);
	    return bundle;
	  }
420
      }
421

422 423
    throw new MissingResourceException("Bundle " + baseName + " not found",
				       baseName, "");
Tom Tromey committed
424
  }
425

Tom Tromey committed
426
  /**
427 428 429 430 431 432 433 434 435
   * Override this method to provide the resource for a keys. This gets
   * called by <code>getObject</code>. If you don't have a resource
   * for the given key, you should return null instead throwing a
   * MissingResourceException. You don't have to ask the parent, getObject()
   * already does this; nor should you throw a MissingResourceException.
   *
   * @param key the key of the resource
   * @return the resource for the key, or null if not in bundle
   * @throws NullPointerException if key is null
Tom Tromey committed
436
   */
437
  protected abstract Object handleGetObject(String key);
Tom Tromey committed
438

Tom Tromey committed
439
  /**
440 441 442 443 444
   * This method should return all keys for which a resource exists; you
   * should include the enumeration of any parent's keys, after filtering out
   * duplicates.
   *
   * @return an enumeration of the keys
Tom Tromey committed
445
   */
446
  public abstract Enumeration getKeys();
Tom Tromey committed
447

Tom Tromey committed
448
  /**
449
   * Tries to load a class or a property file with the specified name.
Tom Tromey committed
450
   *
451 452 453
   * @param localizedName the name
   * @param classloader the classloader
   * @return the resource bundle if it was loaded, otherwise the backup
Tom Tromey committed
454
   */
455 456
  private static ResourceBundle tryBundle(String localizedName,
                                          ClassLoader classloader)
457
  {
458
    ResourceBundle bundle = null;
459 460 461 462 463 464 465
    try
      {
        Class rbClass;
        if (classloader == null)
          rbClass = Class.forName(localizedName);
        else
          rbClass = classloader.loadClass(localizedName);
466 467 468 469 470 471 472 473
	// Note that we do the check up front instead of catching
	// ClassCastException.  The reason for this is that some crazy
	// programs (Eclipse) have classes that do not extend
	// ResourceBundle but that have the same name as a property
	// bundle; in fact Eclipse relies on ResourceBundle not
	// instantiating these classes.
	if (ResourceBundle.class.isAssignableFrom(rbClass))
	  bundle = (ResourceBundle) rbClass.newInstance();
474
      }
475 476 477 478 479
    catch (IllegalAccessException ex) {}
    catch (InstantiationException ex) {}
    catch (ClassNotFoundException ex) {}

    if (bundle == null)
480 481 482 483
      {
	try
	  {
	    InputStream is;
484
	    String resourceName
485 486 487 488 489 490
	      = localizedName.replace('.', '/') + ".properties";
	    if (classloader == null)
	      is = ClassLoader.getSystemResourceAsStream(resourceName);
	    else
	      is = classloader.getResourceAsStream(resourceName);
	    if (is != null)
491
	      bundle = new PropertyResourceBundle(is);
492 493 494
	  }
	catch (IOException ex)
	  {
495
	    MissingResourceException mre = new MissingResourceException
496
	      ("Failed to load bundle: " + localizedName, localizedName, "");
497 498
	    mre.initCause(ex);
	    throw mre;
499 500
	  }
      }
501

502
    return bundle;
503
  }
Tom Tromey committed
504

Tom Tromey committed
505
  /**
506 507 508
   * Tries to load a the bundle for a given locale, also loads the backup
   * locales with the same language.
   *
509
   * @param baseName the raw bundle name, without locale qualifiers
510
   * @param locale the locale
511 512
   * @param classloader the classloader
   * @param bundle the backup (parent) bundle
513 514
   * @param wantBase whether a resource bundle made only from the base name
   *        (with no locale information attached) should be returned.
515
   * @return the resource bundle if it was loaded, otherwise the backup
Tom Tromey committed
516
   */
517 518 519
  private static ResourceBundle tryBundle(String baseName, Locale locale,
                                          ClassLoader classLoader, 
					  boolean wantBase)
520
  {
521 522 523 524 525
    String language = locale.getLanguage();
    String country = locale.getCountry();
    String variant = locale.getVariant();
    
    int baseLen = baseName.length();
526

527 528 529
    // Build up a StringBuffer containing the complete bundle name, fully
    // qualified by locale.
    StringBuffer sb = new StringBuffer(baseLen + variant.length() + 7);
530

531 532
    sb.append(baseName);
    
533 534
    if (language.length() > 0)
      {
535
	sb.append('_');
536
	sb.append(language);
537 538 539 540 541 542 543 544 545 546 547 548
	
	if (country.length() > 0)
	  {
	    sb.append('_');
	    sb.append(country);
	    
	    if (variant.length() > 0)
	      {
	        sb.append('_');
		sb.append(variant);
	      }
	  }
549 550
      }

551 552 553 554 555 556 557
    // Now try to load bundles, starting with the most specialized name.
    // Build up the parent chain as we go.
    String bundleName = sb.toString();
    ResourceBundle first = null; // The most specialized bundle.
    ResourceBundle last = null; // The least specialized bundle.
    
    while (true)
558
      {
559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575
        ResourceBundle foundBundle = tryBundle(bundleName, classLoader);
	if (foundBundle != null)
	  {
	    if (first == null)
	      first = foundBundle;
	    if (last != null)
	      last.parent = foundBundle;
	    foundBundle.locale = locale;
	    last = foundBundle;
	  }
	int idx = bundleName.lastIndexOf('_');
	// Try the non-localized base name only if we already have a
	// localized child bundle, or wantBase is true.
	if (idx > baseLen || (idx == baseLen && (first != null || wantBase)))
	  bundleName = bundleName.substring(0, idx);
	else
	  break;
576
      }
577 578
    
    return first;
579
  }
580
}