1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 * Copyright (c) 2005, 2010, Oracle and/or its affiliates. All rights reserved.
4 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
5 *
6 * This code is free software; you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License version 2 only, as
8 * published by the Free Software Foundation.  Oracle designates this
9 * particular file as subject to the "Classpath" exception as provided
10 * by Oracle in the LICENSE file that accompanied this code.
11 *
12 * This code is distributed in the hope that it will be useful, but WITHOUT
13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
15 * version 2 for more details (a copy is included in the LICENSE file that
16 * accompanied this code).
17 *
18 * You should have received a copy of the GNU General Public License version
19 * 2 along with this work; if not, write to the Free Software Foundation,
20 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
21 *
22 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
23 * or visit www.oracle.com if you need additional information or have any
24 * questions.
25 */
26
27package sun.util;
28
29import java.security.AccessController;
30import java.security.PrivilegedActionException;
31import java.security.PrivilegedExceptionAction;
32import java.util.ArrayList;
33import java.util.HashSet;
34import java.util.IllformedLocaleException;
35import java.util.LinkedHashSet;
36import java.util.List;
37import java.util.Locale;
38import java.util.Locale.Builder;
39import java.util.Map;
40import java.util.ResourceBundle.Control;
41import java.util.ServiceLoader;
42import java.util.Set;
43import java.util.concurrent.ConcurrentHashMap;
44import java.util.concurrent.ConcurrentMap;
45import java.util.spi.LocaleServiceProvider;
46import libcore.icu.ICU;
47
48import sun.util.logging.PlatformLogger;
49import sun.util.resources.OpenListResourceBundle;
50
51/**
52 * An instance of this class holds a set of the third party implementations of a particular
53 * locale sensitive service, such as {@link java.util.spi.LocaleNameProvider}.
54 *
55 */
56public final class LocaleServiceProviderPool {
57
58    /**
59     * A Map that holds singleton instances of this class.  Each instance holds a
60     * set of provider implementations of a particular locale sensitive service.
61     */
62    private static ConcurrentMap<Class<? extends LocaleServiceProvider>, LocaleServiceProviderPool> poolOfPools =
63        new ConcurrentHashMap<>();
64
65    /**
66     * A Set containing locale service providers that implement the
67     * specified provider SPI
68     */
69    private Set<LocaleServiceProvider> providers =
70        new LinkedHashSet<LocaleServiceProvider>();
71
72    /**
73     * A Map that retains Locale->provider mapping
74     */
75    private Map<Locale, LocaleServiceProvider> providersCache =
76        new ConcurrentHashMap<Locale, LocaleServiceProvider>();
77
78    /**
79     * Available locales for this locale sensitive service.  This also contains
80     * JRE's available locales
81     */
82    private Set<Locale> availableLocales = null;
83
84    /**
85     * Available locales within this JRE.  Currently this is declared as
86     * static.  This could be non-static later, so that they could have
87     * different sets for each locale sensitive services.
88     */
89    private static volatile List<Locale> availableJRELocales = null;
90
91    /**
92     * Provider locales for this locale sensitive service.
93     */
94    private Set<Locale> providerLocales = null;
95
96    /**
97     * Special locale for ja_JP with Japanese calendar
98     */
99    private static Locale locale_ja_JP_JP = new Locale("ja", "JP", "JP");
100
101    /**
102     * Special locale for th_TH with Thai numbering system
103     */
104    private static Locale locale_th_TH_TH = new Locale("th", "TH", "TH");
105
106    /**
107     * A factory method that returns a singleton instance
108     */
109    public static LocaleServiceProviderPool getPool(Class<? extends LocaleServiceProvider> providerClass) {
110        LocaleServiceProviderPool pool = poolOfPools.get(providerClass);
111        if (pool == null) {
112            LocaleServiceProviderPool newPool =
113                new LocaleServiceProviderPool(providerClass);
114            pool = poolOfPools.putIfAbsent(providerClass, newPool);
115            if (pool == null) {
116                pool = newPool;
117            }
118        }
119
120        return pool;
121    }
122
123    /**
124     * The sole constructor.
125     *
126     * @param c class of the locale sensitive service
127     */
128    private LocaleServiceProviderPool (final Class<? extends LocaleServiceProvider> c) {
129        try {
130            AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
131                public Object run() {
132                    for (LocaleServiceProvider provider : ServiceLoader.loadInstalled(c)) {
133                        providers.add(provider);
134                    }
135                    return null;
136                }
137            });
138        }  catch (PrivilegedActionException e) {
139            config(e.toString());
140        }
141    }
142
143    private static void config(String message) {
144        PlatformLogger logger = PlatformLogger.getLogger("sun.util.LocaleServiceProviderPool");
145        logger.config(message);
146    }
147
148    /**
149     * Lazy loaded set of available locales.
150     * Loading all locales is a very long operation.
151     *
152     * We know "providerClasses" contains classes that extends LocaleServiceProvider,
153     * but generic array creation is not allowed, thus the "unchecked" warning
154     * is suppressed here.
155     */
156    private static class AllAvailableLocales {
157        /**
158         * Available locales for all locale sensitive services.
159         * This also contains JRE's available locales
160         */
161        static final Locale[] allAvailableLocales;
162
163        static {
164            @SuppressWarnings("unchecked")
165            Class<LocaleServiceProvider>[] providerClasses =
166                        (Class<LocaleServiceProvider>[]) new Class<?>[] {
167                java.text.spi.BreakIteratorProvider.class,
168                java.text.spi.CollatorProvider.class,
169                java.text.spi.DateFormatProvider.class,
170                java.text.spi.DateFormatSymbolsProvider.class,
171                java.text.spi.DecimalFormatSymbolsProvider.class,
172                java.text.spi.NumberFormatProvider.class,
173                java.util.spi.CurrencyNameProvider.class,
174                java.util.spi.LocaleNameProvider.class,
175                java.util.spi.TimeZoneNameProvider.class,
176            };
177
178            // Normalize locales for look up
179            Locale[] allLocales = ICU.getAvailableLocales();
180            Set<Locale> all = new HashSet<Locale>(allLocales.length);
181            for (Locale locale : allLocales) {
182                all.add(getLookupLocale(locale));
183            }
184
185            for (Class<LocaleServiceProvider> providerClass : providerClasses) {
186                LocaleServiceProviderPool pool =
187                    LocaleServiceProviderPool.getPool(providerClass);
188                all.addAll(pool.getProviderLocales());
189            }
190
191            allAvailableLocales = all.toArray(new Locale[0]);
192        }
193    }
194
195    /**
196     * Returns an array of available locales for all the provider classes.
197     * This array is a merged array of all the locales that are provided by each
198     * provider, including the JRE.
199     *
200     * @return an array of the available locales for all provider classes
201     */
202    public static Locale[] getAllAvailableLocales() {
203        return AllAvailableLocales.allAvailableLocales.clone();
204    }
205
206    /**
207     * Returns an array of available locales.  This array is a
208     * merged array of all the locales that are provided by each
209     * provider, including the JRE.
210     *
211     * @return an array of the available locales
212     */
213    public synchronized Locale[] getAvailableLocales() {
214        if (availableLocales == null) {
215            availableLocales = new HashSet<Locale>(getJRELocales());
216            if (hasProviders()) {
217                availableLocales.addAll(getProviderLocales());
218            }
219        }
220        Locale[] tmp = new Locale[availableLocales.size()];
221        availableLocales.toArray(tmp);
222        return tmp;
223    }
224
225    /**
226     * Returns an array of available locales (already normalized
227     * for service lookup) from providers.
228     * Note that this method does not return a defensive copy.
229     *
230     * @return list of the provider locales
231     */
232    private synchronized Set<Locale> getProviderLocales() {
233        if (providerLocales == null) {
234            providerLocales = new HashSet<Locale>();
235            if (hasProviders()) {
236                for (LocaleServiceProvider lsp : providers) {
237                    Locale[] locales = lsp.getAvailableLocales();
238                    for (Locale locale: locales) {
239                        providerLocales.add(getLookupLocale(locale));
240                    }
241                }
242            }
243        }
244        return providerLocales;
245    }
246
247    /**
248     * Returns whether any provider for this locale sensitive
249     * service is available or not.
250     *
251     * @return true if any provider is available
252     */
253    public boolean hasProviders() {
254        return !providers.isEmpty();
255    }
256
257    /**
258     * Returns an array of available locales (already normalized for
259     * service lookup) supported by the JRE.
260     * Note that this method does not return a defensive copy.
261     *
262     * @return list of the available JRE locales
263     */
264    private List<Locale> getJRELocales() {
265        if (availableJRELocales == null) {
266            synchronized (LocaleServiceProviderPool.class) {
267                if (availableJRELocales == null) {
268                    Locale[] allLocales = ICU.getAvailableLocales();
269                    List<Locale> tmpList = new ArrayList<>(allLocales.length);
270                    for (Locale locale : allLocales) {
271                        tmpList.add(getLookupLocale(locale));
272                    }
273                    availableJRELocales = tmpList;
274                }
275            }
276        }
277        return availableJRELocales;
278    }
279
280    /**
281     * Returns whether the given locale is supported by the JRE.
282     *
283     * @param locale the locale to test.
284     * @return true, if the locale is supported by the JRE. false
285     *     otherwise.
286     */
287    private boolean isJRESupported(Locale locale) {
288        List<Locale> locales = getJRELocales();
289        return locales.contains(getLookupLocale(locale));
290    }
291
292    /**
293     * Returns the provider's localized object for the specified
294     * locale.
295     *
296     * @param getter an object on which getObject() method
297     *     is called to obtain the provider's instance.
298     * @param locale the given locale that is used as the starting one
299     * @param params provider specific parameters
300     * @return provider's instance, or null.
301     */
302    public <P, S> S getLocalizedObject(LocalizedObjectGetter<P, S> getter,
303                                     Locale locale,
304                                     Object... params) {
305        return getLocalizedObjectImpl(getter, locale, true, null, null, null, params);
306    }
307
308    /**
309     * Returns the provider's localized name for the specified
310     * locale.
311     *
312     * @param getter an object on which getObject() method
313     *     is called to obtain the provider's instance.
314     * @param locale the given locale that is used as the starting one
315     * @param bundle JRE resource bundle that contains
316     *     the localized names, or null for localized objects.
317     * @param key the key string if bundle is supplied, otherwise null.
318     * @param params provider specific parameters
319     * @return provider's instance, or null.
320     */
321    public <P, S> S getLocalizedObject(LocalizedObjectGetter<P, S> getter,
322                                     Locale locale,
323                                     OpenListResourceBundle bundle,
324                                     String key,
325                                     Object... params) {
326        return getLocalizedObjectImpl(getter, locale, false, null, bundle, key, params);
327    }
328
329    /**
330     * Returns the provider's localized name for the specified
331     * locale.
332     *
333     * @param getter an object on which getObject() method
334     *     is called to obtain the provider's instance.
335     * @param locale the given locale that is used as the starting one
336     * @param bundleKey JRE specific bundle key. e.g., "USD" is for currency
337           symbol and "usd" is for currency display name in the JRE bundle.
338     * @param bundle JRE resource bundle that contains
339     *     the localized names, or null for localized objects.
340     * @param key the key string if bundle is supplied, otherwise null.
341     * @param params provider specific parameters
342     * @return provider's instance, or null.
343     */
344    public <P, S> S getLocalizedObject(LocalizedObjectGetter<P, S> getter,
345                                     Locale locale,
346                                     String bundleKey,
347                                     OpenListResourceBundle bundle,
348                                     String key,
349                                     Object... params) {
350        return getLocalizedObjectImpl(getter, locale, false, bundleKey, bundle, key, params);
351    }
352
353    private <P, S> S getLocalizedObjectImpl(LocalizedObjectGetter<P, S> getter,
354                                     Locale locale,
355                                     boolean isObjectProvider,
356                                     String bundleKey,
357                                     OpenListResourceBundle bundle,
358                                     String key,
359                                     Object... params) {
360        if (hasProviders()) {
361            if (bundleKey == null) {
362                bundleKey = key;
363            }
364            Locale bundleLocale = (bundle != null ? bundle.getLocale() : null);
365            List<Locale> lookupLocales = getLookupLocales(locale);
366            S providersObj = null;
367
368            // check whether a provider has an implementation that's closer
369            // to the requested locale than the bundle we've found (for
370            // localized names), or Java runtime's supported locale
371            // (for localized objects)
372            Set<Locale> provLoc = getProviderLocales();
373            for (int i = 0; i < lookupLocales.size(); i++) {
374                Locale current = lookupLocales.get(i);
375                if (bundleLocale != null) {
376                    if (current.equals(bundleLocale)) {
377                        break;
378                    }
379                } else {
380                    if (isJRESupported(current)) {
381                        break;
382                    }
383                }
384                if (provLoc.contains(current)) {
385                    // It is safe to assume that findProvider() returns the instance of type P.
386                    @SuppressWarnings("unchecked")
387                    P lsp = (P)findProvider(current);
388                    if (lsp != null) {
389                        providersObj = getter.getObject(lsp, locale, key, params);
390                        if (providersObj != null) {
391                            return providersObj;
392                        } else if (isObjectProvider) {
393                            config(
394                                "A locale sensitive service provider returned null for a localized objects,  which should not happen.  provider: " + lsp + " locale: " + locale);
395                        }
396                    }
397                }
398            }
399
400            // look up the JRE bundle and its parent chain.  Only
401            // providers for localized names are checked hereafter.
402            while (bundle != null) {
403                bundleLocale = bundle.getLocale();
404
405                if (bundle.handleGetKeys().contains(bundleKey)) {
406                    // JRE has it.
407                    return null;
408                } else {
409                    // It is safe to assume that findProvider() returns the instance of type P.
410                    @SuppressWarnings("unchecked")
411                    P lsp = (P)findProvider(bundleLocale);
412                    if (lsp != null) {
413                        providersObj = getter.getObject(lsp, locale, key, params);
414                        if (providersObj != null) {
415                            return providersObj;
416                        }
417                    }
418                }
419
420                // try parent bundle
421                bundle = bundle.getParent();
422            }
423        }
424
425        // not found.
426        return null;
427    }
428
429    /**
430     * Returns a locale service provider instance that supports
431     * the specified locale.
432     *
433     * @param locale the given locale
434     * @return the provider, or null if there is
435     *     no provider available.
436     */
437    private LocaleServiceProvider findProvider(Locale locale) {
438        if (!hasProviders()) {
439            return null;
440        }
441
442        if (providersCache.containsKey(locale)) {
443            LocaleServiceProvider provider = providersCache.get(locale);
444            if (provider != NullProvider.INSTANCE) {
445                return provider;
446            }
447        } else {
448            for (LocaleServiceProvider lsp : providers) {
449                Locale[] locales = lsp.getAvailableLocales();
450                for (Locale available: locales) {
451                    // normalize
452                    available = getLookupLocale(available);
453                    if (locale.equals(available)) {
454                        LocaleServiceProvider providerInCache =
455                            providersCache.put(locale, lsp);
456                        return (providerInCache != null ?
457                                providerInCache :
458                                lsp);
459                    }
460                }
461            }
462            providersCache.put(locale, NullProvider.INSTANCE);
463        }
464        return null;
465    }
466
467    /**
468     * Returns a list of candidate locales for service look up.
469     * @param locale the input locale
470     * @return the list of candiate locales for the given locale
471     */
472    private static List<Locale> getLookupLocales(Locale locale) {
473        // Note: We currently use the default implementation of
474        // ResourceBundle.Control.getCandidateLocales. The result
475        // returned by getCandidateLocales are already normalized
476        // (no extensions) for service look up.
477        List<Locale> lookupLocales = new Control(){}.getCandidateLocales("", locale);
478        return lookupLocales;
479    }
480
481    /**
482     * Returns an instance of Locale used for service look up.
483     * The result Locale has no extensions except for ja_JP_JP
484     * and th_TH_TH
485     *
486     * @param locale the locale
487     * @return the locale used for service look up
488     */
489    private static Locale getLookupLocale(Locale locale) {
490        Locale lookupLocale = locale;
491        Set<Character> extensions = locale.getExtensionKeys();
492        if (!extensions.isEmpty()
493                && !locale.equals(locale_ja_JP_JP)
494                && !locale.equals(locale_th_TH_TH)) {
495            // remove extensions
496            Builder locbld = new Builder();
497            try {
498                locbld.setLocale(locale);
499                locbld.clearExtensions();
500                lookupLocale = locbld.build();
501            } catch (IllformedLocaleException e) {
502                // A Locale with non-empty extensions
503                // should have well-formed fields except
504                // for ja_JP_JP and th_TH_TH. Therefore,
505                // it should never enter in this catch clause.
506                config("A locale(" + locale + ") has non-empty extensions, but has illformed fields.");
507
508                // Fallback - script field will be lost.
509                lookupLocale = new Locale(locale.getLanguage(), locale.getCountry(), locale.getVariant());
510            }
511        }
512        return lookupLocale;
513    }
514
515    /**
516     * A dummy locale service provider that indicates there is no
517     * provider available
518     */
519    private static class NullProvider extends LocaleServiceProvider {
520        private static final NullProvider INSTANCE = new NullProvider();
521
522        public Locale[] getAvailableLocales() {
523            throw new RuntimeException("Should not get called.");
524        }
525    }
526
527    /**
528     * An interface to get a localized object for each locale sensitve
529     * service class.
530     */
531    public interface LocalizedObjectGetter<P, S> {
532        /**
533         * Returns an object from the provider
534         *
535         * @param lsp the provider
536         * @param locale the locale
537         * @param key key string to localize, or null if the provider is not
538         *     a name provider
539         * @param params provider specific params
540         * @return localized object from the provider
541         */
542        public S getObject(P lsp,
543                                Locale locale,
544                                String key,
545                                Object... params);
546    }
547}
548