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    }