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