Home | History | Annotate | Download | only in util
      1 /*
      2  *  Licensed to the Apache Software Foundation (ASF) under one or more
      3  *  contributor license agreements.  See the NOTICE file distributed with
      4  *  this work for additional information regarding copyright ownership.
      5  *  The ASF licenses this file to You under the Apache License, Version 2.0
      6  *  (the "License"); you may not use this file except in compliance with
      7  *  the License.  You may obtain a copy of the License at
      8  *
      9  *     http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  *  Unless required by applicable law or agreed to in writing, software
     12  *  distributed under the License is distributed on an "AS IS" BASIS,
     13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  *  See the License for the specific language governing permissions and
     15  *  limitations under the License.
     16  */
     17 
     18 package java.util;
     19 
     20 import dalvik.system.VMStack;
     21 import java.io.File;
     22 import java.io.IOException;
     23 import java.io.InputStream;
     24 import java.io.InputStreamReader;
     25 import java.net.URL;
     26 import java.net.URLConnection;
     27 import java.nio.charset.StandardCharsets;
     28 import libcore.io.IoUtils;
     29 
     30 /**
     31  * {@code ResourceBundle} is an abstract class which is the superclass of classes which
     32  * provide {@code Locale}-specific resources. A bundle contains a number of named
     33  * resources, where the names are {@code Strings}. A bundle may have a parent bundle,
     34  * and when a resource is not found in a bundle, the parent bundle is searched for
     35  * the resource. If the fallback mechanism reaches the base bundle and still
     36  * can't find the resource it throws a {@code MissingResourceException}.
     37  *
     38  * <ul>
     39  * <li>All bundles for the same group of resources share a common base bundle.
     40  * This base bundle acts as the root and is the last fallback in case none of
     41  * its children was able to respond to a request.</li>
     42  * <li>The first level contains changes between different languages. Only the
     43  * differences between a language and the language of the base bundle need to be
     44  * handled by a language-specific {@code ResourceBundle}.</li>
     45  * <li>The second level contains changes between different countries that use
     46  * the same language. Only the differences between a country and the country of
     47  * the language bundle need to be handled by a country-specific {@code ResourceBundle}.
     48  * </li>
     49  * <li>The third level contains changes that don't have a geographic reason
     50  * (e.g. changes that where made at some point in time like {@code PREEURO} where the
     51  * currency of come countries changed. The country bundle would return the
     52  * current currency (Euro) and the {@code PREEURO} variant bundle would return the old
     53  * currency (e.g. DM for Germany).</li>
     54  * </ul>
     55  *
     56  * <strong>Examples</strong>
     57  * <ul>
     58  * <li>BaseName (base bundle)
     59  * <li>BaseName_de (german language bundle)
     60  * <li>BaseName_fr (french language bundle)
     61  * <li>BaseName_de_DE (bundle with Germany specific resources in german)
     62  * <li>BaseName_de_CH (bundle with Switzerland specific resources in german)
     63  * <li>BaseName_fr_CH (bundle with Switzerland specific resources in french)
     64  * <li>BaseName_de_DE_PREEURO (bundle with Germany specific resources in german of
     65  * the time before the Euro)
     66  * <li>BaseName_fr_FR_PREEURO (bundle with France specific resources in french of
     67  * the time before the Euro)
     68  * </ul>
     69  *
     70  * It's also possible to create variants for languages or countries. This can be
     71  * done by just skipping the country or language abbreviation:
     72  * BaseName_us__POSIX or BaseName__DE_PREEURO. But it's not allowed to
     73  * circumvent both language and country: BaseName___VARIANT is illegal.
     74  *
     75  * @see Properties
     76  * @see PropertyResourceBundle
     77  * @see ListResourceBundle
     78  * @since 1.1
     79  */
     80 public abstract class ResourceBundle {
     81 
     82     private static final String UNDER_SCORE = "_";
     83 
     84     private static final String EMPTY_STRING = "";
     85 
     86     /**
     87      * The parent of this {@code ResourceBundle} that is used if this bundle doesn't
     88      * include the requested resource.
     89      */
     90     protected ResourceBundle parent;
     91 
     92     private Locale locale;
     93 
     94     private long lastLoadTime = 0;
     95 
     96     static class MissingBundle extends ResourceBundle {
     97         @Override
     98         public Enumeration<String> getKeys() {
     99             return null;
    100         }
    101 
    102         @Override
    103         public Object handleGetObject(String name) {
    104             return null;
    105         }
    106     }
    107 
    108     private static final ResourceBundle MISSING = new MissingBundle();
    109 
    110     private static final ResourceBundle MISSINGBASE = new MissingBundle();
    111 
    112     private static final WeakHashMap<Object, Hashtable<String, ResourceBundle>> cache
    113             = new WeakHashMap<Object, Hashtable<String, ResourceBundle>>();
    114 
    115     private static Locale cacheLocale = Locale.getDefault();
    116 
    117     /**
    118      * Constructs a new instance of this class.
    119      */
    120     public ResourceBundle() {
    121         /* empty */
    122     }
    123 
    124     /**
    125      * Finds the named resource bundle for the default {@code Locale} and the caller's
    126      * {@code ClassLoader}.
    127      *
    128      * @param bundleName
    129      *            the name of the {@code ResourceBundle}.
    130      * @return the requested {@code ResourceBundle}.
    131      * @throws MissingResourceException
    132      *                if the {@code ResourceBundle} cannot be found.
    133      */
    134     public static ResourceBundle getBundle(String bundleName) throws MissingResourceException {
    135         ClassLoader classLoader = VMStack.getCallingClassLoader();
    136         if (classLoader == null) {
    137             classLoader = getLoader();
    138         }
    139         return getBundle(bundleName, Locale.getDefault(), classLoader);
    140     }
    141 
    142     /**
    143      * Finds the named {@code ResourceBundle} for the specified {@code Locale} and the caller
    144      * {@code ClassLoader}.
    145      *
    146      * @param bundleName
    147      *            the name of the {@code ResourceBundle}.
    148      * @param locale
    149      *            the {@code Locale}.
    150      * @return the requested resource bundle.
    151      * @throws MissingResourceException
    152      *                if the resource bundle cannot be found.
    153      */
    154     public static ResourceBundle getBundle(String bundleName, Locale locale) {
    155         ClassLoader classLoader = VMStack.getCallingClassLoader();
    156         if (classLoader == null) {
    157             classLoader = getLoader();
    158         }
    159         return getBundle(bundleName, locale, classLoader);
    160     }
    161 
    162     /**
    163      * Finds the named resource bundle for the specified {@code Locale} and {@code ClassLoader}.
    164      *
    165      * The passed base name and {@code Locale} are used to create resource bundle names.
    166      * The first name is created by concatenating the base name with the result
    167      * of {@link Locale#toString()}. From this name all parent bundle names are
    168      * derived. Then the same thing is done for the default {@code Locale}. This results
    169      * in a list of possible bundle names.
    170      *
    171      * <strong>Example</strong> For the basename "BaseName", the {@code Locale} of the
    172      * German part of Switzerland (de_CH) and the default {@code Locale} en_US the list
    173      * would look something like this:
    174      *
    175      * <ol>
    176      * <li>BaseName_de_CH</li>
    177      * <li>BaseName_de</li>
    178      * <li>Basename_en_US</li>
    179      * <li>Basename_en</li>
    180      * <li>BaseName</li>
    181      * </ol>
    182      *
    183      * This list also shows the order in which the bundles will be searched for a requested
    184      * resource in the German part of Switzerland (de_CH).
    185      *
    186      * As a first step, this method tries to instantiate
    187      * a {@code ResourceBundle} with the names provided.
    188      * If such a class can be instantiated and initialized, it is returned and
    189      * all the parent bundles are instantiated too. If no such class can be
    190      * found this method tries to load a {@code .properties} file with the names by
    191      * replacing dots in the base name with a slash and by appending
    192      * "{@code .properties}" at the end of the string. If such a resource can be found
    193      * by calling {@link ClassLoader#getResource(String)} it is used to
    194      * initialize a {@link PropertyResourceBundle}. If this succeeds, it will
    195      * also load the parents of this {@code ResourceBundle}.
    196      *
    197      * For compatibility with older code, the bundle name isn't required to be
    198      * a fully qualified class name. It's also possible to directly pass
    199      * the path to a properties file (without a file extension).
    200      *
    201      * @param bundleName
    202      *            the name of the {@code ResourceBundle}.
    203      * @param locale
    204      *            the {@code Locale}.
    205      * @param loader
    206      *            the {@code ClassLoader} to use.
    207      * @return the requested {@code ResourceBundle}.
    208      * @throws MissingResourceException
    209      *                if the {@code ResourceBundle} cannot be found.
    210      */
    211     public static ResourceBundle getBundle(String bundleName, Locale locale,
    212             ClassLoader loader) throws MissingResourceException {
    213         if (loader == null) {
    214             throw new NullPointerException("loader == null");
    215         } else if (bundleName == null) {
    216             throw new NullPointerException("bundleName == null");
    217         }
    218         Locale defaultLocale = Locale.getDefault();
    219         if (!cacheLocale.equals(defaultLocale)) {
    220             cache.clear();
    221             cacheLocale = defaultLocale;
    222         }
    223         ResourceBundle bundle = null;
    224         if (!locale.equals(defaultLocale)) {
    225             bundle = handleGetBundle(false, bundleName, locale, loader);
    226         }
    227         if (bundle == null) {
    228             bundle = handleGetBundle(true, bundleName, defaultLocale, loader);
    229             if (bundle == null) {
    230                 throw missingResourceException(bundleName + '_' + locale, "");
    231             }
    232         }
    233         return bundle;
    234     }
    235 
    236     private static MissingResourceException missingResourceException(String className, String key) {
    237         String detail = "Can't find resource for bundle '" + className + "', key '" + key + "'";
    238         throw new MissingResourceException(detail, className, key);
    239     }
    240 
    241     /**
    242      * Finds the named resource bundle for the specified base name and control.
    243      *
    244      * @param baseName
    245      *            the base name of a resource bundle
    246      * @param control
    247      *            the control that control the access sequence
    248      * @return the named resource bundle
    249      *
    250      * @since 1.6
    251      */
    252     public static ResourceBundle getBundle(String baseName, ResourceBundle.Control control) {
    253         return getBundle(baseName, Locale.getDefault(), getLoader(), control);
    254     }
    255 
    256     /**
    257      * Finds the named resource bundle for the specified base name and control.
    258      *
    259      * @param baseName
    260      *            the base name of a resource bundle
    261      * @param targetLocale
    262      *            the target locale of the resource bundle
    263      * @param control
    264      *            the control that control the access sequence
    265      * @return the named resource bundle
    266      *
    267      * @since 1.6
    268      */
    269     public static ResourceBundle getBundle(String baseName,
    270             Locale targetLocale, ResourceBundle.Control control) {
    271         return getBundle(baseName, targetLocale, getLoader(), control);
    272     }
    273 
    274     private static ClassLoader getLoader() {
    275         ClassLoader cl = ResourceBundle.class.getClassLoader();
    276         if (cl == null) {
    277             cl = ClassLoader.getSystemClassLoader();
    278         }
    279         return cl;
    280     }
    281 
    282     /**
    283      * Finds the named resource bundle for the specified base name and control.
    284      *
    285      * @param baseName
    286      *            the base name of a resource bundle
    287      * @param targetLocale
    288      *            the target locale of the resource bundle
    289      * @param loader
    290      *            the class loader to load resource
    291      * @param control
    292      *            the control that control the access sequence
    293      * @return the named resource bundle
    294      *
    295      * @since 1.6
    296      */
    297     public static ResourceBundle getBundle(String baseName,
    298             Locale targetLocale, ClassLoader loader,
    299             ResourceBundle.Control control) {
    300         boolean expired = false;
    301         String bundleName = control.toBundleName(baseName, targetLocale);
    302         Object cacheKey = loader != null ? loader : "null";
    303         Hashtable<String, ResourceBundle> loaderCache = getLoaderCache(cacheKey);
    304         ResourceBundle result = loaderCache.get(bundleName);
    305         if (result != null) {
    306             long time = control.getTimeToLive(baseName, targetLocale);
    307             if (time == 0 || time == Control.TTL_NO_EXPIRATION_CONTROL
    308                     || time + result.lastLoadTime < System.currentTimeMillis()) {
    309                 if (MISSING == result) {
    310                     throw new MissingResourceException(null, bundleName + '_'
    311                             + targetLocale, EMPTY_STRING);
    312                 }
    313                 return result;
    314             }
    315             expired = true;
    316         }
    317         // try to load
    318         ResourceBundle ret = processGetBundle(baseName, targetLocale, loader,
    319                 control, expired, result);
    320 
    321         if (ret != null) {
    322             loaderCache.put(bundleName, ret);
    323             ret.lastLoadTime = System.currentTimeMillis();
    324             return ret;
    325         }
    326         loaderCache.put(bundleName, MISSING);
    327         throw new MissingResourceException(null, bundleName + '_' + targetLocale, EMPTY_STRING);
    328     }
    329 
    330     private static ResourceBundle processGetBundle(String baseName,
    331             Locale targetLocale, ClassLoader loader,
    332             ResourceBundle.Control control, boolean expired,
    333             ResourceBundle result) {
    334         List<Locale> locales = control.getCandidateLocales(baseName, targetLocale);
    335         if (locales == null) {
    336             throw new IllegalArgumentException();
    337         }
    338         List<String> formats = control.getFormats(baseName);
    339         if (Control.FORMAT_CLASS == formats
    340                 || Control.FORMAT_PROPERTIES == formats
    341                 || Control.FORMAT_DEFAULT == formats) {
    342             throw new IllegalArgumentException();
    343         }
    344         ResourceBundle ret = null;
    345         ResourceBundle currentBundle = null;
    346         ResourceBundle bundle = null;
    347         for (Locale locale : locales) {
    348             for (String format : formats) {
    349                 try {
    350                     if (expired) {
    351                         bundle = control.newBundle(baseName, locale, format,
    352                                 loader, control.needsReload(baseName, locale,
    353                                         format, loader, result, System
    354                                                 .currentTimeMillis()));
    355 
    356                     } else {
    357                         try {
    358                             bundle = control.newBundle(baseName, locale,
    359                                     format, loader, false);
    360                         } catch (IllegalArgumentException e) {
    361                             // do nothing
    362                         }
    363                     }
    364                 } catch (IllegalAccessException e) {
    365                     // do nothing
    366                 } catch (InstantiationException e) {
    367                     // do nothing
    368                 } catch (IOException e) {
    369                     // do nothing
    370                 }
    371                 if (bundle != null) {
    372                     if (currentBundle != null) {
    373                         currentBundle.setParent(bundle);
    374                         currentBundle = bundle;
    375                     } else {
    376                         if (ret == null) {
    377                             ret = bundle;
    378                             currentBundle = ret;
    379                         }
    380                     }
    381                 }
    382                 if (bundle != null) {
    383                     break;
    384                 }
    385             }
    386         }
    387 
    388         if ((ret == null)
    389                 || (Locale.ROOT.equals(ret.getLocale()) && (!(locales.size() == 1 && locales
    390                         .contains(Locale.ROOT))))) {
    391             Locale nextLocale = control.getFallbackLocale(baseName, targetLocale);
    392             if (nextLocale != null) {
    393                 ret = processGetBundle(baseName, nextLocale, loader, control,
    394                         expired, result);
    395             }
    396         }
    397 
    398         return ret;
    399     }
    400 
    401     /**
    402      * Returns the names of the resources contained in this {@code ResourceBundle}.
    403      *
    404      * @return an {@code Enumeration} of the resource names.
    405      */
    406     public abstract Enumeration<String> getKeys();
    407 
    408     /**
    409      * Gets the {@code Locale} of this {@code ResourceBundle}. In case a bundle was not
    410      * found for the requested {@code Locale}, this will return the actual {@code Locale} of
    411      * this resource bundle that was found after doing a fallback.
    412      *
    413      * @return the {@code Locale} of this {@code ResourceBundle}.
    414      */
    415     public Locale getLocale() {
    416         return locale;
    417     }
    418 
    419     /**
    420      * Returns the named resource from this {@code ResourceBundle}. If the resource
    421      * cannot be found in this bundle, it falls back to the parent bundle (if
    422      * it's not null) by calling the {@link #handleGetObject} method. If the resource still
    423      * can't be found it throws a {@code MissingResourceException}.
    424      *
    425      * @param key
    426      *            the name of the resource.
    427      * @return the resource object.
    428      * @throws MissingResourceException
    429      *                if the resource is not found.
    430      */
    431     public final Object getObject(String key) {
    432         ResourceBundle last, theParent = this;
    433         do {
    434             Object result = theParent.handleGetObject(key);
    435             if (result != null) {
    436                 return result;
    437             }
    438             last = theParent;
    439             theParent = theParent.parent;
    440         } while (theParent != null);
    441         throw missingResourceException(last.getClass().getName(), key);
    442     }
    443 
    444     /**
    445      * Returns the named string resource from this {@code ResourceBundle}.
    446      *
    447      * @param key
    448      *            the name of the resource.
    449      * @return the resource string.
    450      * @throws MissingResourceException
    451      *                if the resource is not found.
    452      * @throws ClassCastException
    453      *                if the resource found is not a string.
    454      * @see #getObject(String)
    455      */
    456     public final String getString(String key) {
    457         return (String) getObject(key);
    458     }
    459 
    460     /**
    461      * Returns the named resource from this {@code ResourceBundle}.
    462      *
    463      * @param key
    464      *            the name of the resource.
    465      * @return the resource string array.
    466      * @throws MissingResourceException
    467      *                if the resource is not found.
    468      * @throws ClassCastException
    469      *                if the resource found is not an array of strings.
    470      * @see #getObject(String)
    471      */
    472     public final String[] getStringArray(String key) {
    473         return (String[]) getObject(key);
    474     }
    475 
    476     private static ResourceBundle handleGetBundle(boolean loadBase, String base, Locale locale,
    477             ClassLoader loader) {
    478         String localeName = locale.toString();
    479         String bundleName = localeName.isEmpty()
    480                 ? base
    481                 : (base + "_" + localeName);
    482         Object cacheKey = loader != null ? loader : "null";
    483         Hashtable<String, ResourceBundle> loaderCache = getLoaderCache(cacheKey);
    484         ResourceBundle cached = loaderCache.get(bundleName);
    485         if (cached != null) {
    486             if (cached == MISSINGBASE) {
    487                 return null;
    488             } else if (cached == MISSING) {
    489                 if (!loadBase) {
    490                     return null;
    491                 }
    492                 Locale newLocale = strip(locale);
    493                 if (newLocale == null) {
    494                     return null;
    495                 }
    496                 return handleGetBundle(loadBase, base, newLocale, loader);
    497             }
    498             return cached;
    499         }
    500 
    501         ResourceBundle bundle = null;
    502         try {
    503             Class<?> bundleClass = Class.forName(bundleName, true, loader);
    504             if (ResourceBundle.class.isAssignableFrom(bundleClass)) {
    505                 bundle = (ResourceBundle) bundleClass.newInstance();
    506             }
    507         } catch (LinkageError ignored) {
    508         } catch (Exception ignored) {
    509         }
    510 
    511         if (bundle != null) {
    512             bundle.setLocale(locale);
    513         } else {
    514             String fileName = bundleName.replace('.', '/') + ".properties";
    515             InputStream stream = loader != null
    516                     ? loader.getResourceAsStream(fileName)
    517                     : ClassLoader.getSystemResourceAsStream(fileName);
    518             if (stream != null) {
    519                 try {
    520                     bundle = new PropertyResourceBundle(new InputStreamReader(stream, StandardCharsets.UTF_8));
    521                     bundle.setLocale(locale);
    522                 } catch (IOException ignored) {
    523                 } finally {
    524                     IoUtils.closeQuietly(stream);
    525                 }
    526             }
    527         }
    528 
    529         Locale strippedLocale = strip(locale);
    530         if (bundle != null) {
    531             if (strippedLocale != null) {
    532                 ResourceBundle parent = handleGetBundle(loadBase, base, strippedLocale, loader);
    533                 if (parent != null) {
    534                     bundle.setParent(parent);
    535                 }
    536             }
    537             loaderCache.put(bundleName, bundle);
    538             return bundle;
    539         }
    540 
    541         if (strippedLocale != null && (loadBase || !strippedLocale.toString().isEmpty())) {
    542             bundle = handleGetBundle(loadBase, base, strippedLocale, loader);
    543             if (bundle != null) {
    544                 loaderCache.put(bundleName, bundle);
    545                 return bundle;
    546             }
    547         }
    548         loaderCache.put(bundleName, loadBase ? MISSINGBASE : MISSING);
    549         return null;
    550     }
    551 
    552     private static Hashtable<String, ResourceBundle> getLoaderCache(Object cacheKey) {
    553         synchronized (cache) {
    554             Hashtable<String, ResourceBundle> loaderCache = cache.get(cacheKey);
    555             if (loaderCache == null) {
    556                 loaderCache = new Hashtable<String, ResourceBundle>();
    557                 cache.put(cacheKey, loaderCache);
    558             }
    559             return loaderCache;
    560         }
    561     }
    562 
    563     /**
    564      * Returns the named resource from this {@code ResourceBundle}, or null if the
    565      * resource is not found.
    566      *
    567      * @param key
    568      *            the name of the resource.
    569      * @return the resource object.
    570      */
    571     protected abstract Object handleGetObject(String key);
    572 
    573     /**
    574      * Sets the parent resource bundle of this {@code ResourceBundle}. The parent is
    575      * searched for resources which are not found in this {@code ResourceBundle}.
    576      *
    577      * @param bundle
    578      *            the parent {@code ResourceBundle}.
    579      */
    580     protected void setParent(ResourceBundle bundle) {
    581         parent = bundle;
    582     }
    583 
    584     /**
    585      * Returns a locale with the most-specific field removed, or null if this
    586      * locale had an empty language, country and variant.
    587      */
    588     private static Locale strip(Locale locale) {
    589         String language = locale.getLanguage();
    590         String country = locale.getCountry();
    591         String variant = locale.getVariant();
    592         if (!variant.isEmpty()) {
    593             variant = "";
    594         } else if (!country.isEmpty()) {
    595             country = "";
    596         } else if (!language.isEmpty()) {
    597             language = "";
    598         } else {
    599             return null;
    600         }
    601         return new Locale(language, country, variant);
    602     }
    603 
    604     private void setLocale(Locale locale) {
    605         this.locale = locale;
    606     }
    607 
    608     public static void clearCache() {
    609         cache.remove(ClassLoader.getSystemClassLoader());
    610     }
    611 
    612     public static void clearCache(ClassLoader loader) {
    613         if (loader == null) {
    614             throw new NullPointerException("loader == null");
    615         }
    616         cache.remove(loader);
    617     }
    618 
    619     public boolean containsKey(String key) {
    620         if (key == null) {
    621             throw new NullPointerException("key == null");
    622         }
    623         return keySet().contains(key);
    624     }
    625 
    626     public Set<String> keySet() {
    627         Set<String> ret = new HashSet<String>();
    628         Enumeration<String> keys = getKeys();
    629         while (keys.hasMoreElements()) {
    630             ret.add(keys.nextElement());
    631         }
    632         return ret;
    633     }
    634 
    635     protected Set<String> handleKeySet() {
    636         Set<String> set = keySet();
    637         Set<String> ret = new HashSet<String>();
    638         for (String key : set) {
    639             if (handleGetObject(key) != null) {
    640                 ret.add(key);
    641             }
    642         }
    643         return ret;
    644     }
    645 
    646     private static class NoFallbackControl extends Control {
    647 
    648         static final Control NOFALLBACK_FORMAT_PROPERTIES_CONTROL = new NoFallbackControl(
    649                 JAVAPROPERTIES);
    650 
    651         static final Control NOFALLBACK_FORMAT_CLASS_CONTROL = new NoFallbackControl(
    652                 JAVACLASS);
    653 
    654         static final Control NOFALLBACK_FORMAT_DEFAULT_CONTROL = new NoFallbackControl(
    655                 listDefault);
    656 
    657         public NoFallbackControl(String format) {
    658             listClass = new ArrayList<String>();
    659             listClass.add(format);
    660             super.format = Collections.unmodifiableList(listClass);
    661         }
    662 
    663         public NoFallbackControl(List<String> list) {
    664             super.format = list;
    665         }
    666 
    667         @Override
    668         public Locale getFallbackLocale(String baseName, Locale locale) {
    669             if (baseName == null) {
    670                 throw new NullPointerException("baseName == null");
    671             } else if (locale == null) {
    672                 throw new NullPointerException("locale == null");
    673             }
    674             return null;
    675         }
    676     }
    677 
    678     private static class SimpleControl extends Control {
    679         public SimpleControl(String format) {
    680             listClass = new ArrayList<String>();
    681             listClass.add(format);
    682             super.format = Collections.unmodifiableList(listClass);
    683         }
    684     }
    685 
    686     /**
    687      * ResourceBundle.Control is a static utility class defines ResourceBundle
    688      * load access methods, its default access order is as the same as before.
    689      * However users can implement their own control.
    690      *
    691      * @since 1.6
    692      */
    693     public static class Control {
    694         static List<String> listDefault = new ArrayList<String>();
    695 
    696         static List<String> listClass = new ArrayList<String>();
    697 
    698         static List<String> listProperties = new ArrayList<String>();
    699 
    700         static String JAVACLASS = "java.class";
    701 
    702         static String JAVAPROPERTIES = "java.properties";
    703 
    704         static {
    705             listDefault.add(JAVACLASS);
    706             listDefault.add(JAVAPROPERTIES);
    707             listClass.add(JAVACLASS);
    708             listProperties.add(JAVAPROPERTIES);
    709         }
    710 
    711         /**
    712          * a list defines default format
    713          */
    714         public static final List<String> FORMAT_DEFAULT = Collections
    715                 .unmodifiableList(listDefault);
    716 
    717         /**
    718          * a list defines java class format
    719          */
    720         public static final List<String> FORMAT_CLASS = Collections
    721                 .unmodifiableList(listClass);
    722 
    723         /**
    724          * a list defines property format
    725          */
    726         public static final List<String> FORMAT_PROPERTIES = Collections
    727                 .unmodifiableList(listProperties);
    728 
    729         /**
    730          * a constant that indicates cache will not be used.
    731          */
    732         public static final long TTL_DONT_CACHE = -1L;
    733 
    734         /**
    735          * a constant that indicates cache will not be expired.
    736          */
    737         public static final long TTL_NO_EXPIRATION_CONTROL = -2L;
    738 
    739         private static final Control FORMAT_PROPERTIES_CONTROL = new SimpleControl(
    740                 JAVAPROPERTIES);
    741 
    742         private static final Control FORMAT_CLASS_CONTROL = new SimpleControl(
    743                 JAVACLASS);
    744 
    745         private static final Control FORMAT_DEFAULT_CONTROL = new Control();
    746 
    747         List<String> format;
    748 
    749         /**
    750          * default constructor
    751          *
    752          */
    753         protected Control() {
    754             listClass = new ArrayList<String>();
    755             listClass.add(JAVACLASS);
    756             listClass.add(JAVAPROPERTIES);
    757             format = Collections.unmodifiableList(listClass);
    758         }
    759 
    760         /**
    761          * Returns a control according to {@code formats}.
    762          */
    763         public static Control getControl(List<String> formats) {
    764             switch (formats.size()) {
    765             case 1:
    766                 if (formats.contains(JAVACLASS)) {
    767                     return FORMAT_CLASS_CONTROL;
    768                 }
    769                 if (formats.contains(JAVAPROPERTIES)) {
    770                     return FORMAT_PROPERTIES_CONTROL;
    771                 }
    772                 break;
    773             case 2:
    774                 if (formats.equals(FORMAT_DEFAULT)) {
    775                     return FORMAT_DEFAULT_CONTROL;
    776                 }
    777                 break;
    778             }
    779             throw new IllegalArgumentException();
    780         }
    781 
    782         /**
    783          * Returns a control according to {@code formats} whose fallback
    784          * locale is null.
    785          */
    786         public static Control getNoFallbackControl(List<String> formats) {
    787             switch (formats.size()) {
    788             case 1:
    789                 if (formats.contains(JAVACLASS)) {
    790                     return NoFallbackControl.NOFALLBACK_FORMAT_CLASS_CONTROL;
    791                 }
    792                 if (formats.contains(JAVAPROPERTIES)) {
    793                     return NoFallbackControl.NOFALLBACK_FORMAT_PROPERTIES_CONTROL;
    794                 }
    795                 break;
    796             case 2:
    797                 if (formats.equals(FORMAT_DEFAULT)) {
    798                     return NoFallbackControl.NOFALLBACK_FORMAT_DEFAULT_CONTROL;
    799                 }
    800                 break;
    801             }
    802             throw new IllegalArgumentException();
    803         }
    804 
    805         /**
    806          * Returns a list of candidate locales according to {@code baseName} in
    807          * {@code locale}.
    808          */
    809         public List<Locale> getCandidateLocales(String baseName, Locale locale) {
    810             if (baseName == null) {
    811                 throw new NullPointerException("baseName == null");
    812             } else if (locale == null) {
    813                 throw new NullPointerException("locale == null");
    814             }
    815             List<Locale> retList = new ArrayList<Locale>();
    816             String language = locale.getLanguage();
    817             String country = locale.getCountry();
    818             String variant = locale.getVariant();
    819             if (!EMPTY_STRING.equals(variant)) {
    820                 retList.add(new Locale(language, country, variant));
    821             }
    822             if (!EMPTY_STRING.equals(country)) {
    823                 retList.add(new Locale(language, country));
    824             }
    825             if (!EMPTY_STRING.equals(language)) {
    826                 retList.add(new Locale(language));
    827             }
    828             retList.add(Locale.ROOT);
    829             return retList;
    830         }
    831 
    832         /**
    833          * Returns a list of strings of formats according to {@code baseName}.
    834          */
    835         public List<String> getFormats(String baseName) {
    836             if (baseName == null) {
    837                 throw new NullPointerException("baseName == null");
    838             }
    839             return format;
    840         }
    841 
    842         /**
    843          * Returns the fallback locale for {@code baseName} in {@code locale}.
    844          */
    845         public Locale getFallbackLocale(String baseName, Locale locale) {
    846             if (baseName == null) {
    847                 throw new NullPointerException("baseName == null");
    848             } else if (locale == null) {
    849                 throw new NullPointerException("locale == null");
    850             }
    851             if (Locale.getDefault() != locale) {
    852                 return Locale.getDefault();
    853             }
    854             return null;
    855         }
    856 
    857         /**
    858          * Returns a new ResourceBundle.
    859          *
    860          * @param baseName
    861          *            the base name to use
    862          * @param locale
    863          *            the given locale
    864          * @param format
    865          *            the format, default is "java.class" or "java.properties"
    866          * @param loader
    867          *            the classloader to use
    868          * @param reload
    869          *            whether to reload the resource
    870          * @return a new ResourceBundle according to the give parameters
    871          * @throws IllegalAccessException
    872          *             if we can not access resources
    873          * @throws InstantiationException
    874          *             if we can not instantiate a resource class
    875          * @throws IOException
    876          *             if other I/O exception happens
    877          */
    878         public ResourceBundle newBundle(String baseName, Locale locale,
    879                 String format, ClassLoader loader, boolean reload)
    880                 throws IllegalAccessException, InstantiationException,
    881                 IOException {
    882             if (format == null) {
    883                 throw new NullPointerException("format == null");
    884             } else if (loader == null) {
    885                 throw new NullPointerException("loader == null");
    886             }
    887             final String bundleName = toBundleName(baseName, locale);
    888             final ClassLoader clsloader = loader;
    889             ResourceBundle ret;
    890             if (format.equals(JAVACLASS)) {
    891                 Class<?> cls = null;
    892                 try {
    893                     cls = clsloader.loadClass(bundleName);
    894                 } catch (Exception e) {
    895                 } catch (NoClassDefFoundError e) {
    896                 }
    897                 if (cls == null) {
    898                     return null;
    899                 }
    900                 try {
    901                     ResourceBundle bundle = (ResourceBundle) cls.newInstance();
    902                     bundle.setLocale(locale);
    903                     return bundle;
    904                 } catch (NullPointerException e) {
    905                     return null;
    906                 }
    907             }
    908             if (format.equals(JAVAPROPERTIES)) {
    909                 InputStream streams = null;
    910                 final String resourceName = toResourceName(bundleName, "properties");
    911                 if (reload) {
    912                     URL url = null;
    913                     try {
    914                         url = loader.getResource(resourceName);
    915                     } catch (NullPointerException e) {
    916                         // do nothing
    917                     }
    918                     if (url != null) {
    919                         URLConnection con = url.openConnection();
    920                         con.setUseCaches(false);
    921                         streams = con.getInputStream();
    922                     }
    923                 } else {
    924                     try {
    925                         streams = clsloader.getResourceAsStream(resourceName);
    926                     } catch (NullPointerException e) {
    927                         // do nothing
    928                     }
    929                 }
    930                 if (streams != null) {
    931                     try {
    932                         ret = new PropertyResourceBundle(new InputStreamReader(streams));
    933                         ret.setLocale(locale);
    934                         streams.close();
    935                     } catch (IOException e) {
    936                         return null;
    937                     }
    938                     return ret;
    939                 }
    940                 return null;
    941             }
    942             throw new IllegalArgumentException();
    943         }
    944 
    945         /**
    946          * Returns the time to live of the ResourceBundle {@code baseName} in {@code locale},
    947          * default is TTL_NO_EXPIRATION_CONTROL.
    948          */
    949         public long getTimeToLive(String baseName, Locale locale) {
    950             if (baseName == null) {
    951                 throw new NullPointerException("baseName == null");
    952             } else if (locale == null) {
    953                 throw new NullPointerException("locale == null");
    954             }
    955             return TTL_NO_EXPIRATION_CONTROL;
    956         }
    957 
    958         /**
    959          * Returns true if the ResourceBundle needs to reload.
    960          *
    961          * @param baseName
    962          *            the base name of the ResourceBundle
    963          * @param locale
    964          *            the locale of the ResourceBundle
    965          * @param format
    966          *            the format to load
    967          * @param loader
    968          *            the ClassLoader to load resource
    969          * @param bundle
    970          *            the ResourceBundle
    971          * @param loadTime
    972          *            the expired time
    973          * @return if the ResourceBundle needs to reload
    974          */
    975         public boolean needsReload(String baseName, Locale locale,
    976                 String format, ClassLoader loader, ResourceBundle bundle,
    977                 long loadTime) {
    978             if (bundle == null) {
    979                 // FIXME what's the use of bundle?
    980                 throw new NullPointerException("bundle == null");
    981             }
    982             String bundleName = toBundleName(baseName, locale);
    983             String suffix = format;
    984             if (format.equals(JAVACLASS)) {
    985                 suffix = "class";
    986             }
    987             if (format.equals(JAVAPROPERTIES)) {
    988                 suffix = "properties";
    989             }
    990             String urlname = toResourceName(bundleName, suffix);
    991             URL url = loader.getResource(urlname);
    992             if (url != null) {
    993                 String fileName = url.getFile();
    994                 long lastModified = new File(fileName).lastModified();
    995                 if (lastModified > loadTime) {
    996                     return true;
    997                 }
    998             }
    999             return false;
   1000         }
   1001 
   1002         /**
   1003          * a utility method to answer the name of a resource bundle according to
   1004          * the given base name and locale
   1005          *
   1006          * @param baseName
   1007          *            the given base name
   1008          * @param locale
   1009          *            the locale to use
   1010          * @return the name of a resource bundle according to the given base
   1011          *         name and locale
   1012          */
   1013         public String toBundleName(String baseName, Locale locale) {
   1014             final String emptyString = EMPTY_STRING;
   1015             final String preString = UNDER_SCORE;
   1016             final String underline = UNDER_SCORE;
   1017             if (baseName == null) {
   1018                 throw new NullPointerException("baseName == null");
   1019             }
   1020             StringBuilder ret = new StringBuilder();
   1021             StringBuilder prefix = new StringBuilder();
   1022             ret.append(baseName);
   1023             if (!locale.getLanguage().equals(emptyString)) {
   1024                 ret.append(underline);
   1025                 ret.append(locale.getLanguage());
   1026             } else {
   1027                 prefix.append(preString);
   1028             }
   1029             if (!locale.getCountry().equals(emptyString)) {
   1030                 ret.append((CharSequence) prefix);
   1031                 ret.append(underline);
   1032                 ret.append(locale.getCountry());
   1033                 prefix = new StringBuilder();
   1034             } else {
   1035                 prefix.append(preString);
   1036             }
   1037             if (!locale.getVariant().equals(emptyString)) {
   1038                 ret.append((CharSequence) prefix);
   1039                 ret.append(underline);
   1040                 ret.append(locale.getVariant());
   1041             }
   1042             return ret.toString();
   1043         }
   1044 
   1045         /**
   1046          * a utility method to answer the name of a resource according to the
   1047          * given bundleName and suffix
   1048          *
   1049          * @param bundleName
   1050          *            the given bundle name
   1051          * @param suffix
   1052          *            the suffix
   1053          * @return the name of a resource according to the given bundleName and
   1054          *         suffix
   1055          */
   1056         public final String toResourceName(String bundleName, String suffix) {
   1057             if (suffix == null) {
   1058                 throw new NullPointerException("suffix == null");
   1059             }
   1060             StringBuilder ret = new StringBuilder(bundleName.replace('.', '/'));
   1061             ret.append('.');
   1062             ret.append(suffix);
   1063             return ret.toString();
   1064         }
   1065     }
   1066 }
   1067