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 27 package sun.util; 28 29 import java.security.AccessController; 30 import java.security.PrivilegedActionException; 31 import java.security.PrivilegedExceptionAction; 32 import java.util.ArrayList; 33 import java.util.HashSet; 34 import java.util.IllformedLocaleException; 35 import java.util.LinkedHashSet; 36 import java.util.List; 37 import java.util.Locale; 38 import java.util.Locale.Builder; 39 import java.util.Map; 40 import java.util.ResourceBundle.Control; 41 import java.util.ServiceLoader; 42 import java.util.Set; 43 import java.util.concurrent.ConcurrentHashMap; 44 import java.util.concurrent.ConcurrentMap; 45 import java.util.spi.LocaleServiceProvider; 46 import libcore.icu.ICU; 47 48 import sun.util.logging.PlatformLogger; 49 import 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 */ 56 public 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 */ getPool(Class<? extends LocaleServiceProvider> providerClass)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 */ LocaleServiceProviderPool(final Class<? extends LocaleServiceProvider> c)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 config(String message)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) { getLookupLocale(locale)182 all.add(getLookupLocale(locale)); 183 } 184 185 for (Class<LocaleServiceProvider> providerClass : providerClasses) { 186 LocaleServiceProviderPool pool = 187 LocaleServiceProviderPool.getPool(providerClass); pool.getProviderLocales()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 */ getAllAvailableLocales()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 */ getAvailableLocales()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 */ getProviderLocales()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 */ hasProviders()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 */ getJRELocales()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 */ isJRESupported(Locale locale)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 */ getLocalizedObject(LocalizedObjectGetter<P, S> getter, Locale locale, Object... params)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 */ getLocalizedObject(LocalizedObjectGetter<P, S> getter, Locale locale, OpenListResourceBundle bundle, String key, Object... params)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 */ getLocalizedObject(LocalizedObjectGetter<P, S> getter, Locale locale, String bundleKey, OpenListResourceBundle bundle, String key, Object... params)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 getLocalizedObjectImpl(LocalizedObjectGetter<P, S> getter, Locale locale, boolean isObjectProvider, String bundleKey, OpenListResourceBundle bundle, String key, Object... params)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 */ findProvider(Locale locale)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 */ getLookupLocales(Locale locale)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 */ getLookupLocale(Locale locale)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 getAvailableLocales()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 */ getObject(P lsp, Locale locale, String key, Object... params)542 public S getObject(P lsp, 543 Locale locale, 544 String key, 545 Object... params); 546 } 547 } 548