OSDN Git Service

2003-04-29 Michael Koch <konqueror@gmx.de>
[pf3gnuchains/gcc-fork.git] / libjava / java / util / ResourceBundle.java
index 0c078cc..7a4a282 100644 (file)
-/* Copyright (C) 1998, 1999  Cygnus Solutions
+/* ResourceBundle -- aids in loading resource bundles
+   Copyright (C) 1998, 1999, 2001, 2002, 2003 Free Software Foundation, Inc.
+
+This file is part of GNU Classpath.
+
+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.
+
+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.
+
+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.
+
+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. */
 
-   This file is part of libgcj.
-
-This software is copyrighted work licensed under the terms of the
-Libgcj License.  Please consult the file "LIBGCJ_LICENSE" for
-details.  */
 
 package java.util;
 
+import java.lang.ref.Reference;
+import java.lang.ref.SoftReference;
 import java.io.InputStream;
+import java.io.IOException;
+import gnu.classpath.Configuration;
 
 /**
- * @author Anthony Green <green@cygnus.com>
- * @date November 26, 1998.
+ * 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
+ * is searched in following order (<i>def. language<i> stands for the
+ * two letter ISO language code of the default locale (see
+ * <code>Locale.getDefault()</code>).
+ *
+<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>
+ *
+ * <p>A bundle is backed up by less specific bundles (omitting variant, country
+ * or language). But it is not backed up by the default language locale.
+ *
+ * <p>If you provide a bundle for a given locale, say
+ * <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>.
+ *
+ * <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)
+ * @see Locale
+ * @see ListResourceBundle
+ * @see PropertyResourceBundle
+ * @since 1.1
+ * @status updated to 1.4
  */
-
-/* Written using "Java Class Libraries", 2nd edition, ISBN 0-201-31002-3,
- * and "The Java Language Specification", ISBN 0-201-63451-1.  */
-
 public abstract class ResourceBundle
 {
+  /**
+   * 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.
+   */
   protected ResourceBundle parent;
 
-  // This is used to cache resource bundles.
-  private static Hashtable resource_cache = new Hashtable ();
-
-  public ResourceBundle ()
-    {
-    }
-
-  public final String getString (String key) throws MissingResourceException
-    {
-      return (String) getObject(key);
-    }
-
-  public final String[] getStringArray (String key) 
-    throws MissingResourceException
-    {
-      return (String[]) getObject(key);
-    }
-
-  public final Object getObject(String key) throws MissingResourceException
-    {
-      Object result;
-
-      try 
-       {
-         return handleGetObject (key);
-       }
+  /**
+   * The locale of this resource bundle. You can read this with
+   * <code>getLocale</code> and it is automatically set in
+   * <code>getBundle</code>.
+   */
+  private Locale locale;
+
+  private static native ClassLoader getCallingClassLoader();
+
+  /**
+   * The resource bundle cache. This is a two-level hash map: The key
+   * is the class loader, the value is a new HashMap. The key of this
+   * second hash map is the localized name, the value is a soft
+   * references to the resource bundle.
+   */
+  private static Map resourceBundleCache;
+
+  /**
+   * The last default Locale we saw. If this ever changes then we have to
+   * reset our caches.
+   */
+  private static Locale lastDefaultLocale;
+
+  /**
+   * The `empty' locale is created once in order to optimize
+   * tryBundle().
+   */
+  private static final Locale emptyLocale = new Locale("");
+
+  /**
+   * The constructor. It does nothing special.
+   */
+  public ResourceBundle()
+  {
+  }
+
+  /**
+   * 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
+   */
+  public final String getString(String key)
+  {
+    return (String) getObject(key);
+  }
+
+  /**
+   * Get an array of Strings from this resource bundle. This method
+   * provides a convenient way to get it 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
+   */
+  public final String[] getStringArray(String key)
+  {
+    return (String[]) getObject(key);
+  }
+
+  /**
+   * 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
+   */
+  public final Object getObject(String key)
+  {
+    for (ResourceBundle bundle = this; bundle != null; bundle = bundle.parent)
+      try
+        {
+          Object o = bundle.handleGetObject(key);
+          if (o != null)
+            return o;
+        }
       catch (MissingResourceException ex)
-       {
-         if (parent != null)
-           return parent.getObject(key);
-         else 
-           throw ex;
-       }
-    }
-
-  public static final ResourceBundle getBundle(String baseName) 
-    throws MissingResourceException
-    {
-      return getBundle(baseName, Locale.getDefault());
-    }
-
-  // Start searching with the name bundleName.  Continue searching by
-  // stripping off the '_' delimited tails until the search name is
-  // the same as stopHere.
-  private static final ResourceBundle trySomeGetBundle (String bundleName,
-                                                       String stopHere)
-    {
-      Class rbc;
-      ResourceBundle needs_parent = null, r, result = null;
-
-      while (true)
-       {
-         try 
-           {
-             rbc = Class.forName(bundleName);
-             r = null;
-             try 
-               {
-                 r = (ResourceBundle) rbc.newInstance();
-               }
-             catch (IllegalAccessException ex)
-               {
-                 // Fall through
-               }
-             catch (InstantiationException ex)
-               {
-                 // Fall through
-               }
-             if (r != null)
-               {
-                 if (result == null)
-                   result = r;
-                 if (needs_parent != null)
-                   {
-                     // We've been through the loop one or more times
-                     // already.  Set the parent and keep going.
-                     needs_parent.setParent(r);
-                   }
-                 needs_parent = r;
-               }
-           }
-         catch (ClassNotFoundException ex)
-           {
-             // Fall through.
-           }
-
-         // Look for a properties file.
+        {
+        }
+    throw new MissingResourceException("Key not found", getClass().getName(),
+                                      key);
+  }
+
+  /**
+   * 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
+   */
+  public Locale getLocale()
+  {
+    return locale;
+  }
+
+  /**
+   * 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
+   */
+  protected void setParent(ResourceBundle parent)
+  {
+    this.parent = parent;
+  }
+
+  /**
+   * 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
+   */
+  public static final ResourceBundle getBundle(String baseName)
+  {
+    return getBundle(baseName, Locale.getDefault(),
+                     getCallingClassLoader());
+  }
+
+  /**
+   * 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.
+   *
+   * @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
+   */
+  public static final ResourceBundle getBundle(String baseName,
+                                               Locale locale)
+  {
+    return getBundle(baseName, locale, getCallingClassLoader());
+  }
+
+  /**
+   * 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
+   * locale, and the suffix 2 means the string from the default locale:<ul>
+   * <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>
+   * <li>baseName + "_" + language2<li>
+   * <li>baseName</li>
+   * </ul>
+   *
+   * <p>In the sequence, entries with an empty string are ignored. Next,
+   * <code>getBundle</code> tries to instantiate the resource bundle:<ul>
+   * <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
+   * has a parent).
+   *
+   * <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
+   * Locale("en", "UK").
+   *
+   * <p>Calling getBundle with the shown locale argument values instantiates
+   * resource bundles from the following sources:<ul>
+   * <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>
+   * The file MyResources_fr_CH.properties is never used because it is hidden
+   * by MyResources_fr_CH.class.
+   *
+   * @param baseName the name of the ResourceBundle
+   * @param locale A locale
+   * @param classloader a ClassLoader
+   * @return the desired resource bundle
+   * @throws MissingResourceException if the resource bundle can't be found
+   * @throws NullPointerException if any argument is null
+   * @since 1.2
+   */
+  // This method is synchronized so that the cache is properly
+  // handled.
+  public static final synchronized ResourceBundle getBundle
+    (String baseName, Locale locale, ClassLoader classLoader)
+  {
+    // This implementation searches the bundle in the reverse direction
+    // and builds the parent chain on the fly.
+    Locale defaultLocale = Locale.getDefault();
+    if (defaultLocale != lastDefaultLocale)
+      {
+       resourceBundleCache = new HashMap();
+       lastDefaultLocale = defaultLocale;
+      }
+    HashMap cache = (HashMap) resourceBundleCache.get(classLoader);
+    StringBuffer sb = new StringBuffer(60);
+    sb.append(baseName).append('_').append(locale);
+    String name = sb.toString();
+
+    if (cache == null)
+      {
+        cache = new HashMap();
+        resourceBundleCache.put(classLoader, cache);
+      }
+    else if (cache.containsKey(name))
+      {
+       Reference ref = (Reference) cache.get(name);
+       ResourceBundle result = null;
+       // If REF is null, that means that we added a `null' value to
+       // the hash map.  That means we failed to find the bundle
+       // previously, and we cached that fact.  The JDK does this, so
+       // it must be ok.
+       if (ref == null)
+         throw new MissingResourceException("Bundle " + baseName
+                                            + " not found",
+                                            baseName, "");
+       else
          {
-           InputStream i = 
-               ClassLoader.getSystemResourceAsStream (bundleName.replace ('.', '/') 
-                                                      + ".properties");
-           if (i != null)
+           ResourceBundle rb = (ResourceBundle) ref.get();
+           if (rb != null)
              {
-               try {
-                 return new PropertyResourceBundle (i);
-               } catch (java.io.IOException e) {
-                 // The docs don't appear to define what happens in
-                 // this case, but it seems like continuing the
-                 // search is a reasonable thing to do.
-               }
+               // RB should already have the right parent, except if
+               // something very strange happened.
+               return rb;
              }
+           // If RB is null, then we previously found it but it was
+           // collected.  So we try again.
          }
-
-         if (bundleName.equals(stopHere))
-           return result;
-         else
-           {
-             int last = bundleName.lastIndexOf('_');
-                 
-             // No more underscores?
-             if (last == -1)
-               return result;
-
-             // Loop around, testing this new shorter name.
-             bundleName = bundleName.substring(0, last);
-           }
-       }
-    }
-
-  // Search for bundles, but stop at baseName_language (if required).
-  // This is synchronized so that the cache works correctly.
-  private static final synchronized ResourceBundle
-    partialGetBundle (String baseName, Locale locale, boolean langStop)
-    {
-      ResourceBundle rb;
-
-      // Explicitly invoke locale.toString() to force a
-      // NullPointerException when required.
-      String bundleName = baseName + "_" + locale.toString();
-
-      // Check the cache.
-      Object obj = resource_cache.get(bundleName);
-      if (obj != null)
-       return (ResourceBundle) obj;
-
-      String stopHere = (baseName 
-                        + (langStop ? ("_" + locale.getLanguage()) : ""));
-
-
-      rb = trySomeGetBundle(bundleName, stopHere);
-      if (rb != null)
-       resource_cache.put(bundleName, rb);
-
-      return rb;
-    }
-
-  public static final ResourceBundle getBundle (String baseName, 
-                                               Locale locale)
-    throws MissingResourceException
-    {
-      ResourceBundle rb;
-      Class rbc;
-
-      if (baseName == null)
-       throw new NullPointerException ();
-
-      rb = partialGetBundle(baseName, locale, false);
-      if (rb != null)
-       return rb;
-
-      // Finally, try the default locale.
-      if (! locale.equals(Locale.getDefault()))
-       {
-         rb = partialGetBundle(baseName, Locale.getDefault(), true);
-         if (rb != null)
-           return rb;
-       }
-
-      throw new MissingResourceException("can't load bundle", 
-                                        baseName,
-                                        "bundle");
-    }
-
-  protected void setParent(ResourceBundle parent)
-    {
-      this.parent = parent;
-    }
-
-  protected abstract Object handleGetObject(String key) 
-    throws MissingResourceException;
-
+      }
+
+    // It is ok if this returns null.  We aren't required to have the
+    // base bundle.
+    ResourceBundle baseBundle = tryBundle(baseName, emptyLocale,
+                                          classLoader, null, cache);
+
+    // Now use our locale, followed by the default locale.  We only
+    // need to try the default locale if our locale is different, and
+    // if our locale failed to yield a result other than the base
+    // bundle.
+    ResourceBundle bundle = tryLocalBundle(baseName, locale,
+                                           classLoader, baseBundle, cache);
+    if (bundle == baseBundle && !locale.equals(defaultLocale))
+      {
+       bundle = tryLocalBundle(baseName, defaultLocale,
+                               classLoader, baseBundle, cache);
+       // We need to record that the argument locale maps to the
+       // bundle we just found.  If we didn't find a bundle, record
+       // that instead.
+       if (bundle == null)
+         cache.put(name, null);
+       else
+         cache.put(name, new SoftReference(bundle));
+      }
+
+    if (bundle == null)
+      throw new MissingResourceException("Bundle " + baseName + " not found",
+                                        baseName, "");
+
+    return bundle;
+  }
+
+  /**
+   * 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
+   */
+  protected abstract Object handleGetObject(String key);
+
+  /**
+   * 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
+   */
   public abstract Enumeration getKeys();
+
+  /**
+   * Tries to load a class or a property file with the specified name.
+   *
+   * @param localizedName the name
+   * @param locale the locale, that must be used exactly
+   * @param classloader the classloader
+   * @param bundle the backup (parent) bundle
+   * @return the resource bundle if it was loaded, otherwise the backup
+   */
+  private static final ResourceBundle tryBundle(String localizedName,
+                                                Locale locale,
+                                                ClassLoader classloader,
+                                                ResourceBundle bundle,
+                                                HashMap cache)
+  {
+    // First look into the cache.
+    if (cache.containsKey(localizedName))
+      {
+       Reference ref = (Reference) cache.get(localizedName);
+       ResourceBundle result = null;
+       // If REF is null, that means that we added a `null' value to
+       // the hash map.  That means we failed to find the bundle
+       // previously, and we cached that fact.  The JDK does this, so
+       // it must be ok.
+       if (ref == null)
+         return null;
+       else
+         {
+           ResourceBundle rb = (ResourceBundle) ref.get();
+           if (rb != null)
+             {
+               // RB should already have the right parent, except if
+               // something very strange happened.
+               return rb;
+             }
+           // If RB is null, then we previously found it but it was
+           // collected.  So we try again.
+         }
+      }
+
+    // foundBundle holds exact matches for the localizedName resource
+    // bundle, which may later be cached.
+    ResourceBundle foundBundle = null;
+    try
+      {
+        Class rbClass;
+        if (classloader == null)
+          rbClass = Class.forName(localizedName);
+        else
+          rbClass = classloader.loadClass(localizedName);
+        foundBundle = (ResourceBundle) rbClass.newInstance();
+        foundBundle.parent = bundle;
+        foundBundle.locale = locale;
+      }
+    catch (Exception ex)
+      {
+        // ignore them all
+       foundBundle = null;
+      }
+    if (foundBundle == null)
+      {
+       try
+         {
+           InputStream is;
+           final String resourceName
+             = localizedName.replace('.', '/') + ".properties";
+           if (classloader == null)
+             is = ClassLoader.getSystemResourceAsStream(resourceName);
+           else
+             is = classloader.getResourceAsStream(resourceName);
+           if (is != null)
+             {
+               foundBundle = new PropertyResourceBundle(is);
+               foundBundle.parent = bundle;
+               foundBundle.locale = locale;
+             }
+         }
+       catch (IOException ex)
+         {
+         }
+      }
+
+    // Put the result into the hash table.  If we didn't find anything
+    // here, we record our parent bundle.  If we record `null' that means
+    // nothing, not even the base, was found.
+    if (foundBundle == null)
+      foundBundle = bundle;
+    if (foundBundle == null)
+      cache.put(localizedName, null);
+    else
+      cache.put(localizedName, new SoftReference(foundBundle));
+    return foundBundle;
+  }
+
+  /**
+   * Tries to load a the bundle for a given locale, also loads the backup
+   * locales with the same language.
+   *
+   * @param name the name
+   * @param locale the locale
+   * @param classloader the classloader
+   * @param bundle the backup (parent) bundle
+   * @return the resource bundle if it was loaded, otherwise the backup
+   */
+  private static final ResourceBundle tryLocalBundle(String baseName,
+                                                    Locale locale,
+                                                     ClassLoader classloader,
+                                                     ResourceBundle bundle,
+                                                     HashMap cache)
+  {
+    final String language = locale.getLanguage();
+    final String country = locale.getCountry();
+    final String variant = locale.getVariant();
+
+    StringBuffer sb = new StringBuffer(60);
+    sb.append(baseName);
+    sb.append('_');
+
+    if (language.length() > 0)
+      {
+       sb.append(language);
+       bundle = tryBundle(sb.toString(), new Locale(language),
+                          classloader, bundle, cache);
+      }
+    // If LANGUAGE was empty, we still need to try the other
+    // components, and the `_' is required.
+    sb.append('_');
+
+    if (country.length() > 0)
+      {
+       sb.append(country);
+       bundle = tryBundle(sb.toString(), new Locale(language, country),
+                          classloader, bundle, cache);
+      }
+    sb.append('_');
+
+    if (variant.length() > 0)
+      {
+       sb.append(variant);
+       bundle = tryBundle(sb.toString(), locale,
+                          classloader, bundle, cache);
+      }
+
+    return bundle;
+  }
 }