1/* 2 * Copyright (c) 2015, 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/* 27 * (C) Copyright Taligent, Inc. 1996, 1997 - All Rights Reserved 28 * (C) Copyright IBM Corp. 1996 - 1999 - All Rights Reserved 29 * 30 * The original version of this source code and documentation 31 * is copyrighted and owned by Taligent, Inc., a wholly-owned 32 * subsidiary of IBM. These materials are provided under terms 33 * of a License Agreement between Taligent and Sun. This technology 34 * is protected by multiple US and International patents. 35 * 36 * This notice and attribution to Taligent may not be removed. 37 * Taligent is a registered trademark of Taligent, Inc. 38 * 39 */ 40 41package sun.util.resources; 42 43import java.lang.ref.ReferenceQueue; 44import java.lang.ref.SoftReference; 45import java.security.AccessController; 46import java.security.PrivilegedAction; 47import java.util.Enumeration; 48import java.util.Iterator; 49import java.util.List; 50import java.util.Locale; 51import java.util.MissingResourceException; 52import java.util.Objects; 53import java.util.ResourceBundle; 54import java.util.ServiceConfigurationError; 55import java.util.ServiceLoader; 56import java.util.concurrent.ConcurrentHashMap; 57import java.util.concurrent.ConcurrentMap; 58import java.util.spi.ResourceBundleProvider; 59import jdk.internal.misc.JavaUtilResourceBundleAccess; 60import jdk.internal.misc.SharedSecrets; 61 62/** 63 */ 64public abstract class Bundles { 65 66 /** initial size of the bundle cache */ 67 private static final int INITIAL_CACHE_SIZE = 32; 68 69 /** constant indicating that no resource bundle exists */ 70 private static final ResourceBundle NONEXISTENT_BUNDLE = new ResourceBundle() { 71 @Override 72 public Enumeration<String> getKeys() { return null; } 73 @Override 74 protected Object handleGetObject(String key) { return null; } 75 @Override 76 public String toString() { return "NONEXISTENT_BUNDLE"; } 77 }; 78 79 private static final JavaUtilResourceBundleAccess bundleAccess 80 = SharedSecrets.getJavaUtilResourceBundleAccess(); 81 82 /** 83 * The cache is a map from cache keys (with bundle base name, locale, and 84 * class loader) to either a resource bundle or NONEXISTENT_BUNDLE wrapped by a 85 * BundleReference. 86 * 87 * The cache is a ConcurrentMap, allowing the cache to be searched 88 * concurrently by multiple threads. This will also allow the cache keys 89 * to be reclaimed along with the ClassLoaders they reference. 90 * 91 * This variable would be better named "cache", but we keep the old 92 * name for compatibility with some workarounds for bug 4212439. 93 */ 94 private static final ConcurrentMap<CacheKey, BundleReference> cacheList 95 = new ConcurrentHashMap<>(INITIAL_CACHE_SIZE); 96 97 /** 98 * Queue for reference objects referring to class loaders or bundles. 99 */ 100 private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>(); 101 102 private Bundles() { 103 } 104 105 public static ResourceBundle of(String baseName, Locale locale, Strategy strategy) { 106 return loadBundleOf(baseName, locale, strategy); 107 } 108 109 private static ResourceBundle loadBundleOf(String baseName, 110 Locale targetLocale, 111 Strategy strategy) { 112 Objects.requireNonNull(baseName); 113 Objects.requireNonNull(targetLocale); 114 Objects.requireNonNull(strategy); 115 116 CacheKey cacheKey = new CacheKey(baseName, targetLocale); 117 118 ResourceBundle bundle = null; 119 120 // Quick lookup of the cache. 121 BundleReference bundleRef = cacheList.get(cacheKey); 122 if (bundleRef != null) { 123 bundle = bundleRef.get(); 124 } 125 126 // If this bundle and all of its parents are valid, 127 // then return this bundle. 128 if (isValidBundle(bundle)) { 129 return bundle; 130 } 131 132 // Get the providers for loading the "leaf" bundle (i.e., bundle for 133 // targetLocale). If no providers are required for the bundle, 134 // none of its parents will require providers. 135 Class<? extends ResourceBundleProvider> type 136 = strategy.getResourceBundleProviderType(baseName, targetLocale); 137 if (type != null) { 138 @SuppressWarnings("unchecked") 139 ServiceLoader<ResourceBundleProvider> providers 140 = (ServiceLoader<ResourceBundleProvider>) ServiceLoader.loadInstalled(type); 141 cacheKey.setProviders(providers); 142 } 143 144 List<Locale> candidateLocales = strategy.getCandidateLocales(baseName, targetLocale); 145 bundle = findBundleOf(cacheKey, strategy, baseName, candidateLocales, 0); 146 if (bundle == null) { 147 throwMissingResourceException(baseName, targetLocale, cacheKey.getCause()); 148 } 149 return bundle; 150 } 151 152 private static ResourceBundle findBundleOf(CacheKey cacheKey, 153 Strategy strategy, 154 String baseName, 155 List<Locale> candidateLocales, 156 int index) { 157 ResourceBundle parent = null; 158 Locale targetLocale = candidateLocales.get(index); 159 if (index != candidateLocales.size() - 1) { 160 parent = findBundleOf(cacheKey, strategy, baseName, candidateLocales, index + 1); 161 } 162 163 // Before we do the real loading work, see whether we need to 164 // do some housekeeping: If resource bundles have been nulled out, 165 // remove all related information from the cache. 166 cleanupCache(); 167 168 // find an individual ResourceBundle in the cache 169 cacheKey.setLocale(targetLocale); 170 ResourceBundle bundle = findBundleInCache(cacheKey); 171 if (bundle != null) { 172 if (bundle == NONEXISTENT_BUNDLE) { 173 return parent; 174 } 175 if (bundleAccess.getParent(bundle) == parent) { 176 return bundle; 177 } 178 // Remove bundle from the cache. 179 BundleReference bundleRef = cacheList.get(cacheKey); 180 if (bundleRef != null && bundleRef.get() == bundle) { 181 cacheList.remove(cacheKey, bundleRef); 182 } 183 } 184 185 // Determine if providers should be used for loading the bundle. 186 // An assumption here is that if the leaf bundle of a look-up path is 187 // in java.base, all bundles of the path are in java.base. 188 // (e.g., en_US of path en_US -> en -> root is in java.base and the rest 189 // are in java.base as well) 190 // This assumption isn't valid for general bundle loading. 191 ServiceLoader<ResourceBundleProvider> providers = cacheKey.getProviders(); 192 if (providers != null) { 193 if (strategy.getResourceBundleProviderType(baseName, targetLocale) == null) { 194 providers = null; 195 } 196 } 197 198 CacheKey constKey = (CacheKey) cacheKey.clone(); 199 try { 200 if (providers != null) { 201 bundle = loadBundleFromProviders(baseName, targetLocale, providers, cacheKey); 202 } else { 203 try { 204 String bundleName = strategy.toBundleName(baseName, targetLocale); 205 Class<?> c = Class.forName(Bundles.class.getModule(), bundleName); 206 if (c != null && ResourceBundle.class.isAssignableFrom(c)) { 207 @SuppressWarnings("unchecked") 208 Class<ResourceBundle> bundleClass = (Class<ResourceBundle>) c; 209 bundle = bundleAccess.newResourceBundle(bundleClass); 210 } 211 } catch (Exception e) { 212 cacheKey.setCause(e); 213 } 214 } 215 } finally { 216 if (constKey.getCause() instanceof InterruptedException) { 217 Thread.currentThread().interrupt(); 218 } 219 } 220 221 if (bundle == null) { 222 // Put NONEXISTENT_BUNDLE in the cache as a mark that there's no bundle 223 // instance for the locale. 224 putBundleInCache(cacheKey, NONEXISTENT_BUNDLE); 225 return parent; 226 } 227 228 if (parent != null && bundleAccess.getParent(bundle) == null) { 229 bundleAccess.setParent(bundle, parent); 230 } 231 bundleAccess.setLocale(bundle, targetLocale); 232 bundleAccess.setName(bundle, baseName); 233 bundle = putBundleInCache(cacheKey, bundle); 234 return bundle; 235 } 236 237 private static void cleanupCache() { 238 Object ref; 239 while ((ref = referenceQueue.poll()) != null) { 240 cacheList.remove(((CacheKeyReference)ref).getCacheKey()); 241 } 242 } 243 244 /** 245 * Loads ResourceBundle from service providers. 246 */ 247 private static ResourceBundle loadBundleFromProviders(String baseName, 248 Locale locale, 249 ServiceLoader<ResourceBundleProvider> providers, 250 CacheKey cacheKey) 251 { 252 return AccessController.doPrivileged( 253 new PrivilegedAction<>() { 254 public ResourceBundle run() { 255 for (Iterator<ResourceBundleProvider> itr = providers.iterator(); itr.hasNext(); ) { 256 try { 257 ResourceBundleProvider provider = itr.next(); 258 ResourceBundle bundle = provider.getBundle(baseName, locale); 259 if (bundle != null) { 260 return bundle; 261 } 262 } catch (ServiceConfigurationError | SecurityException e) { 263 if (cacheKey != null) { 264 cacheKey.setCause(e); 265 } 266 } 267 } 268 return null; 269 } 270 }); 271 272 } 273 274 private static boolean isValidBundle(ResourceBundle bundle) { 275 return bundle != null && bundle != NONEXISTENT_BUNDLE; 276 } 277 278 /** 279 * Throw a MissingResourceException with proper message 280 */ 281 private static void throwMissingResourceException(String baseName, 282 Locale locale, 283 Throwable cause) { 284 // If the cause is a MissingResourceException, avoid creating 285 // a long chain. (6355009) 286 if (cause instanceof MissingResourceException) { 287 cause = null; 288 } 289 MissingResourceException e; 290 e = new MissingResourceException("Can't find bundle for base name " 291 + baseName + ", locale " + locale, 292 baseName + "_" + locale, // className 293 ""); 294 e.initCause(cause); 295 throw e; 296 } 297 298 /** 299 * Finds a bundle in the cache. 300 * 301 * @param cacheKey the key to look up the cache 302 * @return the ResourceBundle found in the cache or null 303 */ 304 private static ResourceBundle findBundleInCache(CacheKey cacheKey) { 305 BundleReference bundleRef = cacheList.get(cacheKey); 306 if (bundleRef == null) { 307 return null; 308 } 309 return bundleRef.get(); 310 } 311 312 /** 313 * Put a new bundle in the cache. 314 * 315 * @param cacheKey the key for the resource bundle 316 * @param bundle the resource bundle to be put in the cache 317 * @return the ResourceBundle for the cacheKey; if someone has put 318 * the bundle before this call, the one found in the cache is 319 * returned. 320 */ 321 private static ResourceBundle putBundleInCache(CacheKey cacheKey, 322 ResourceBundle bundle) { 323 CacheKey key = (CacheKey) cacheKey.clone(); 324 BundleReference bundleRef = new BundleReference(bundle, referenceQueue, key); 325 326 // Put the bundle in the cache if it's not been in the cache. 327 BundleReference result = cacheList.putIfAbsent(key, bundleRef); 328 329 // If someone else has put the same bundle in the cache before 330 // us, we should use the one in the cache. 331 if (result != null) { 332 ResourceBundle rb = result.get(); 333 if (rb != null) { 334 // Clear the back link to the cache key 335 bundle = rb; 336 // Clear the reference in the BundleReference so that 337 // it won't be enqueued. 338 bundleRef.clear(); 339 } else { 340 // Replace the invalid (garbage collected) 341 // instance with the valid one. 342 cacheList.put(key, bundleRef); 343 } 344 } 345 return bundle; 346 } 347 348 349 /** 350 * The Strategy interface defines methods that are called by Bundles.of during 351 * the resource bundle loading process. 352 */ 353 public static interface Strategy { 354 /** 355 * Returns a list of locales to be looked up for bundle loading. 356 */ 357 public List<Locale> getCandidateLocales(String baseName, Locale locale); 358 359 /** 360 * Returns the bundle name for the given baseName and locale. 361 */ 362 public String toBundleName(String baseName, Locale locale); 363 364 /** 365 * Returns the service provider type for the given baseName 366 * and locale, or null if no service providers should be used. 367 */ 368 public Class<? extends ResourceBundleProvider> getResourceBundleProviderType(String baseName, 369 Locale locale); 370 } 371 372 /** 373 * The common interface to get a CacheKey in LoaderReference and 374 * BundleReference. 375 */ 376 private static interface CacheKeyReference { 377 public CacheKey getCacheKey(); 378 } 379 380 /** 381 * References to bundles are soft references so that they can be garbage 382 * collected when they have no hard references. 383 */ 384 private static class BundleReference extends SoftReference<ResourceBundle> 385 implements CacheKeyReference { 386 private final CacheKey cacheKey; 387 388 BundleReference(ResourceBundle referent, ReferenceQueue<Object> q, CacheKey key) { 389 super(referent, q); 390 cacheKey = key; 391 } 392 393 @Override 394 public CacheKey getCacheKey() { 395 return cacheKey; 396 } 397 } 398 399 /** 400 * Key used for cached resource bundles. The key checks the base 401 * name, the locale, and the class loader to determine if the 402 * resource is a match to the requested one. The loader may be 403 * null, but the base name and the locale must have a non-null 404 * value. 405 */ 406 private static class CacheKey implements Cloneable { 407 // These two are the actual keys for lookup in Map. 408 private String name; 409 private Locale locale; 410 411 // Placeholder for an error report by a Throwable 412 private Throwable cause; 413 414 // Hash code value cache to avoid recalculating the hash code 415 // of this instance. 416 private int hashCodeCache; 417 418 // The service loader to load bundles or null if no service loader 419 // is required. 420 private ServiceLoader<ResourceBundleProvider> providers; 421 422 CacheKey(String baseName, Locale locale) { 423 this.name = baseName; 424 this.locale = locale; 425 calculateHashCode(); 426 } 427 428 String getName() { 429 return name; 430 } 431 432 CacheKey setName(String baseName) { 433 if (!this.name.equals(baseName)) { 434 this.name = baseName; 435 calculateHashCode(); 436 } 437 return this; 438 } 439 440 Locale getLocale() { 441 return locale; 442 } 443 444 CacheKey setLocale(Locale locale) { 445 if (!this.locale.equals(locale)) { 446 this.locale = locale; 447 calculateHashCode(); 448 } 449 return this; 450 } 451 452 ServiceLoader<ResourceBundleProvider> getProviders() { 453 return providers; 454 } 455 456 void setProviders(ServiceLoader<ResourceBundleProvider> providers) { 457 this.providers = providers; 458 } 459 460 @Override 461 public boolean equals(Object other) { 462 if (this == other) { 463 return true; 464 } 465 try { 466 final CacheKey otherEntry = (CacheKey)other; 467 //quick check to see if they are not equal 468 if (hashCodeCache != otherEntry.hashCodeCache) { 469 return false; 470 } 471 return locale.equals(otherEntry.locale) 472 && name.equals(otherEntry.name); 473 } catch (NullPointerException | ClassCastException e) { 474 } 475 return false; 476 } 477 478 @Override 479 public int hashCode() { 480 return hashCodeCache; 481 } 482 483 private void calculateHashCode() { 484 hashCodeCache = name.hashCode() << 3; 485 hashCodeCache ^= locale.hashCode(); 486 } 487 488 @Override 489 public Object clone() { 490 try { 491 CacheKey clone = (CacheKey) super.clone(); 492 // Clear the reference to a Throwable 493 clone.cause = null; 494 // Clear the reference to a ServiceLoader 495 clone.providers = null; 496 return clone; 497 } catch (CloneNotSupportedException e) { 498 //this should never happen 499 throw new InternalError(e); 500 } 501 } 502 503 private void setCause(Throwable cause) { 504 if (this.cause == null) { 505 this.cause = cause; 506 } else { 507 // Override the cause if the previous one is 508 // ClassNotFoundException. 509 if (this.cause instanceof ClassNotFoundException) { 510 this.cause = cause; 511 } 512 } 513 } 514 515 private Throwable getCause() { 516 return cause; 517 } 518 519 @Override 520 public String toString() { 521 String l = locale.toString(); 522 if (l.isEmpty()) { 523 if (!locale.getVariant().isEmpty()) { 524 l = "__" + locale.getVariant(); 525 } else { 526 l = "\"\""; 527 } 528 } 529 return "CacheKey[" + name + ", lc=" + l + ")]"; 530 } 531 } 532} 533