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 
  41 package sun.util.resources;
  42 
  43 import java.lang.ref.ReferenceQueue;
  44 import java.lang.ref.SoftReference;
  45 import java.security.AccessController;
  46 import java.security.PrivilegedAction;
  47 import java.util.Enumeration;
  48 import java.util.Iterator;
  49 import java.util.List;
  50 import java.util.Locale;
  51 import java.util.MissingResourceException;
  52 import java.util.Objects;
  53 import java.util.ResourceBundle;
  54 import java.util.ServiceConfigurationError;
  55 import java.util.ServiceLoader;
  56 import java.util.concurrent.ConcurrentHashMap;
  57 import java.util.concurrent.ConcurrentMap;
  58 import java.util.spi.ResourceBundleProvider;
  59 import jdk.internal.misc.JavaUtilResourceBundleAccess;
  60 import jdk.internal.misc.SharedSecrets;
  61 
  62 /**
  63  */
  64 public 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 }