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