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
18package java.util;
19
20import dalvik.system.VMStack;
21import java.io.File;
22import java.io.IOException;
23import java.io.InputStream;
24import java.io.InputStreamReader;
25import java.net.URL;
26import java.net.URLConnection;
27import java.nio.charset.StandardCharsets;
28import 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 */
80public 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