| /* |
| * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| /* |
| * (C) Copyright Taligent, Inc. 1996, 1997 - All Rights Reserved |
| * (C) Copyright IBM Corp. 1996 - 1999 - All Rights Reserved |
| * |
| * The original version of this source code and documentation |
| * is copyrighted and owned by Taligent, Inc., a wholly-owned |
| * subsidiary of IBM. These materials are provided under terms |
| * of a License Agreement between Taligent and Sun. This technology |
| * is protected by multiple US and International patents. |
| * |
| * This notice and attribution to Taligent may not be removed. |
| * Taligent is a registered trademark of Taligent, Inc. |
| * |
| */ |
| |
| package sun.util.resources; |
| |
| import java.lang.ref.ReferenceQueue; |
| import java.lang.ref.SoftReference; |
| import java.security.AccessController; |
| import java.security.PrivilegedAction; |
| import java.util.Enumeration; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.MissingResourceException; |
| import java.util.Objects; |
| import java.util.ResourceBundle; |
| import java.util.ServiceConfigurationError; |
| import java.util.ServiceLoader; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.ConcurrentMap; |
| import java.util.spi.ResourceBundleProvider; |
| import jdk.internal.misc.JavaUtilResourceBundleAccess; |
| import jdk.internal.misc.SharedSecrets; |
| |
| /** |
| */ |
| public abstract class Bundles { |
| |
| /** initial size of the bundle cache */ |
| private static final int INITIAL_CACHE_SIZE = 32; |
| |
| /** constant indicating that no resource bundle exists */ |
| private static final ResourceBundle NONEXISTENT_BUNDLE = new ResourceBundle() { |
| @Override |
| public Enumeration<String> getKeys() { return null; } |
| @Override |
| protected Object handleGetObject(String key) { return null; } |
| @Override |
| public String toString() { return "NONEXISTENT_BUNDLE"; } |
| }; |
| |
| private static final JavaUtilResourceBundleAccess bundleAccess |
| = SharedSecrets.getJavaUtilResourceBundleAccess(); |
| |
| /** |
| * The cache is a map from cache keys (with bundle base name, locale, and |
| * class loader) to either a resource bundle or NONEXISTENT_BUNDLE wrapped by a |
| * BundleReference. |
| * |
| * The cache is a ConcurrentMap, allowing the cache to be searched |
| * concurrently by multiple threads. This will also allow the cache keys |
| * to be reclaimed along with the ClassLoaders they reference. |
| * |
| * This variable would be better named "cache", but we keep the old |
| * name for compatibility with some workarounds for bug 4212439. |
| */ |
| private static final ConcurrentMap<CacheKey, BundleReference> cacheList |
| = new ConcurrentHashMap<>(INITIAL_CACHE_SIZE); |
| |
| /** |
| * Queue for reference objects referring to class loaders or bundles. |
| */ |
| private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>(); |
| |
| private Bundles() { |
| } |
| |
| public static ResourceBundle of(String baseName, Locale locale, Strategy strategy) { |
| return loadBundleOf(baseName, locale, strategy); |
| } |
| |
| private static ResourceBundle loadBundleOf(String baseName, |
| Locale targetLocale, |
| Strategy strategy) { |
| Objects.requireNonNull(baseName); |
| Objects.requireNonNull(targetLocale); |
| Objects.requireNonNull(strategy); |
| |
| CacheKey cacheKey = new CacheKey(baseName, targetLocale); |
| |
| ResourceBundle bundle = null; |
| |
| // Quick lookup of the cache. |
| BundleReference bundleRef = cacheList.get(cacheKey); |
| if (bundleRef != null) { |
| bundle = bundleRef.get(); |
| } |
| |
| // If this bundle and all of its parents are valid, |
| // then return this bundle. |
| if (isValidBundle(bundle)) { |
| return bundle; |
| } |
| |
| // Get the providers for loading the "leaf" bundle (i.e., bundle for |
| // targetLocale). If no providers are required for the bundle, |
| // none of its parents will require providers. |
| Class<? extends ResourceBundleProvider> type |
| = strategy.getResourceBundleProviderType(baseName, targetLocale); |
| if (type != null) { |
| @SuppressWarnings("unchecked") |
| ServiceLoader<ResourceBundleProvider> providers |
| = (ServiceLoader<ResourceBundleProvider>) ServiceLoader.loadInstalled(type); |
| cacheKey.setProviders(providers); |
| } |
| |
| List<Locale> candidateLocales = strategy.getCandidateLocales(baseName, targetLocale); |
| bundle = findBundleOf(cacheKey, strategy, baseName, candidateLocales, 0); |
| if (bundle == null) { |
| throwMissingResourceException(baseName, targetLocale, cacheKey.getCause()); |
| } |
| return bundle; |
| } |
| |
| private static ResourceBundle findBundleOf(CacheKey cacheKey, |
| Strategy strategy, |
| String baseName, |
| List<Locale> candidateLocales, |
| int index) { |
| ResourceBundle parent = null; |
| Locale targetLocale = candidateLocales.get(index); |
| if (index != candidateLocales.size() - 1) { |
| parent = findBundleOf(cacheKey, strategy, baseName, candidateLocales, index + 1); |
| } |
| |
| // Before we do the real loading work, see whether we need to |
| // do some housekeeping: If resource bundles have been nulled out, |
| // remove all related information from the cache. |
| cleanupCache(); |
| |
| // find an individual ResourceBundle in the cache |
| cacheKey.setLocale(targetLocale); |
| ResourceBundle bundle = findBundleInCache(cacheKey); |
| if (bundle != null) { |
| if (bundle == NONEXISTENT_BUNDLE) { |
| return parent; |
| } |
| if (bundleAccess.getParent(bundle) == parent) { |
| return bundle; |
| } |
| // Remove bundle from the cache. |
| BundleReference bundleRef = cacheList.get(cacheKey); |
| if (bundleRef != null && bundleRef.get() == bundle) { |
| cacheList.remove(cacheKey, bundleRef); |
| } |
| } |
| |
| // Determine if providers should be used for loading the bundle. |
| // An assumption here is that if the leaf bundle of a look-up path is |
| // in java.base, all bundles of the path are in java.base. |
| // (e.g., en_US of path en_US -> en -> root is in java.base and the rest |
| // are in java.base as well) |
| // This assumption isn't valid for general bundle loading. |
| ServiceLoader<ResourceBundleProvider> providers = cacheKey.getProviders(); |
| if (providers != null) { |
| if (strategy.getResourceBundleProviderType(baseName, targetLocale) == null) { |
| providers = null; |
| } |
| } |
| |
| CacheKey constKey = (CacheKey) cacheKey.clone(); |
| try { |
| if (providers != null) { |
| bundle = loadBundleFromProviders(baseName, targetLocale, providers, cacheKey); |
| } else { |
| try { |
| String bundleName = strategy.toBundleName(baseName, targetLocale); |
| Class<?> c = Class.forName(Bundles.class.getModule(), bundleName); |
| if (c != null && ResourceBundle.class.isAssignableFrom(c)) { |
| @SuppressWarnings("unchecked") |
| Class<ResourceBundle> bundleClass = (Class<ResourceBundle>) c; |
| bundle = bundleAccess.newResourceBundle(bundleClass); |
| } |
| } catch (Exception e) { |
| cacheKey.setCause(e); |
| } |
| } |
| } finally { |
| if (constKey.getCause() instanceof InterruptedException) { |
| Thread.currentThread().interrupt(); |
| } |
| } |
| |
| if (bundle == null) { |
| // Put NONEXISTENT_BUNDLE in the cache as a mark that there's no bundle |
| // instance for the locale. |
| putBundleInCache(cacheKey, NONEXISTENT_BUNDLE); |
| return parent; |
| } |
| |
| if (parent != null && bundleAccess.getParent(bundle) == null) { |
| bundleAccess.setParent(bundle, parent); |
| } |
| bundleAccess.setLocale(bundle, targetLocale); |
| bundleAccess.setName(bundle, baseName); |
| bundle = putBundleInCache(cacheKey, bundle); |
| return bundle; |
| } |
| |
| private static void cleanupCache() { |
| Object ref; |
| while ((ref = referenceQueue.poll()) != null) { |
| cacheList.remove(((CacheKeyReference)ref).getCacheKey()); |
| } |
| } |
| |
| /** |
| * Loads ResourceBundle from service providers. |
| */ |
| private static ResourceBundle loadBundleFromProviders(String baseName, |
| Locale locale, |
| ServiceLoader<ResourceBundleProvider> providers, |
| CacheKey cacheKey) |
| { |
| return AccessController.doPrivileged( |
| new PrivilegedAction<>() { |
| public ResourceBundle run() { |
| for (Iterator<ResourceBundleProvider> itr = providers.iterator(); itr.hasNext(); ) { |
| try { |
| ResourceBundleProvider provider = itr.next(); |
| ResourceBundle bundle = provider.getBundle(baseName, locale); |
| if (bundle != null) { |
| return bundle; |
| } |
| } catch (ServiceConfigurationError | SecurityException e) { |
| if (cacheKey != null) { |
| cacheKey.setCause(e); |
| } |
| } |
| } |
| return null; |
| } |
| }); |
| |
| } |
| |
| private static boolean isValidBundle(ResourceBundle bundle) { |
| return bundle != null && bundle != NONEXISTENT_BUNDLE; |
| } |
| |
| /** |
| * Throw a MissingResourceException with proper message |
| */ |
| private static void throwMissingResourceException(String baseName, |
| Locale locale, |
| Throwable cause) { |
| // If the cause is a MissingResourceException, avoid creating |
| // a long chain. (6355009) |
| if (cause instanceof MissingResourceException) { |
| cause = null; |
| } |
| MissingResourceException e; |
| e = new MissingResourceException("Can't find bundle for base name " |
| + baseName + ", locale " + locale, |
| baseName + "_" + locale, // className |
| ""); |
| e.initCause(cause); |
| throw e; |
| } |
| |
| /** |
| * Finds a bundle in the cache. |
| * |
| * @param cacheKey the key to look up the cache |
| * @return the ResourceBundle found in the cache or null |
| */ |
| private static ResourceBundle findBundleInCache(CacheKey cacheKey) { |
| BundleReference bundleRef = cacheList.get(cacheKey); |
| if (bundleRef == null) { |
| return null; |
| } |
| return bundleRef.get(); |
| } |
| |
| /** |
| * Put a new bundle in the cache. |
| * |
| * @param cacheKey the key for the resource bundle |
| * @param bundle the resource bundle to be put in the cache |
| * @return the ResourceBundle for the cacheKey; if someone has put |
| * the bundle before this call, the one found in the cache is |
| * returned. |
| */ |
| private static ResourceBundle putBundleInCache(CacheKey cacheKey, |
| ResourceBundle bundle) { |
| CacheKey key = (CacheKey) cacheKey.clone(); |
| BundleReference bundleRef = new BundleReference(bundle, referenceQueue, key); |
| |
| // Put the bundle in the cache if it's not been in the cache. |
| BundleReference result = cacheList.putIfAbsent(key, bundleRef); |
| |
| // If someone else has put the same bundle in the cache before |
| // us, we should use the one in the cache. |
| if (result != null) { |
| ResourceBundle rb = result.get(); |
| if (rb != null) { |
| // Clear the back link to the cache key |
| bundle = rb; |
| // Clear the reference in the BundleReference so that |
| // it won't be enqueued. |
| bundleRef.clear(); |
| } else { |
| // Replace the invalid (garbage collected) |
| // instance with the valid one. |
| cacheList.put(key, bundleRef); |
| } |
| } |
| return bundle; |
| } |
| |
| |
| /** |
| * The Strategy interface defines methods that are called by Bundles.of during |
| * the resource bundle loading process. |
| */ |
| public static interface Strategy { |
| /** |
| * Returns a list of locales to be looked up for bundle loading. |
| */ |
| public List<Locale> getCandidateLocales(String baseName, Locale locale); |
| |
| /** |
| * Returns the bundle name for the given baseName and locale. |
| */ |
| public String toBundleName(String baseName, Locale locale); |
| |
| /** |
| * Returns the service provider type for the given baseName |
| * and locale, or null if no service providers should be used. |
| */ |
| public Class<? extends ResourceBundleProvider> getResourceBundleProviderType(String baseName, |
| Locale locale); |
| } |
| |
| /** |
| * The common interface to get a CacheKey in LoaderReference and |
| * BundleReference. |
| */ |
| private static interface CacheKeyReference { |
| public CacheKey getCacheKey(); |
| } |
| |
| /** |
| * References to bundles are soft references so that they can be garbage |
| * collected when they have no hard references. |
| */ |
| private static class BundleReference extends SoftReference<ResourceBundle> |
| implements CacheKeyReference { |
| private final CacheKey cacheKey; |
| |
| BundleReference(ResourceBundle referent, ReferenceQueue<Object> q, CacheKey key) { |
| super(referent, q); |
| cacheKey = key; |
| } |
| |
| @Override |
| public CacheKey getCacheKey() { |
| return cacheKey; |
| } |
| } |
| |
| /** |
| * Key used for cached resource bundles. The key checks the base |
| * name, the locale, and the class loader to determine if the |
| * resource is a match to the requested one. The loader may be |
| * null, but the base name and the locale must have a non-null |
| * value. |
| */ |
| private static class CacheKey implements Cloneable { |
| // These two are the actual keys for lookup in Map. |
| private String name; |
| private Locale locale; |
| |
| // Placeholder for an error report by a Throwable |
| private Throwable cause; |
| |
| // Hash code value cache to avoid recalculating the hash code |
| // of this instance. |
| private int hashCodeCache; |
| |
| // The service loader to load bundles or null if no service loader |
| // is required. |
| private ServiceLoader<ResourceBundleProvider> providers; |
| |
| CacheKey(String baseName, Locale locale) { |
| this.name = baseName; |
| this.locale = locale; |
| calculateHashCode(); |
| } |
| |
| String getName() { |
| return name; |
| } |
| |
| CacheKey setName(String baseName) { |
| if (!this.name.equals(baseName)) { |
| this.name = baseName; |
| calculateHashCode(); |
| } |
| return this; |
| } |
| |
| Locale getLocale() { |
| return locale; |
| } |
| |
| CacheKey setLocale(Locale locale) { |
| if (!this.locale.equals(locale)) { |
| this.locale = locale; |
| calculateHashCode(); |
| } |
| return this; |
| } |
| |
| ServiceLoader<ResourceBundleProvider> getProviders() { |
| return providers; |
| } |
| |
| void setProviders(ServiceLoader<ResourceBundleProvider> providers) { |
| this.providers = providers; |
| } |
| |
| @Override |
| public boolean equals(Object other) { |
| if (this == other) { |
| return true; |
| } |
| try { |
| final CacheKey otherEntry = (CacheKey)other; |
| //quick check to see if they are not equal |
| if (hashCodeCache != otherEntry.hashCodeCache) { |
| return false; |
| } |
| return locale.equals(otherEntry.locale) |
| && name.equals(otherEntry.name); |
| } catch (NullPointerException | ClassCastException e) { |
| } |
| return false; |
| } |
| |
| @Override |
| public int hashCode() { |
| return hashCodeCache; |
| } |
| |
| private void calculateHashCode() { |
| hashCodeCache = name.hashCode() << 3; |
| hashCodeCache ^= locale.hashCode(); |
| } |
| |
| @Override |
| public Object clone() { |
| try { |
| CacheKey clone = (CacheKey) super.clone(); |
| // Clear the reference to a Throwable |
| clone.cause = null; |
| // Clear the reference to a ServiceLoader |
| clone.providers = null; |
| return clone; |
| } catch (CloneNotSupportedException e) { |
| //this should never happen |
| throw new InternalError(e); |
| } |
| } |
| |
| private void setCause(Throwable cause) { |
| if (this.cause == null) { |
| this.cause = cause; |
| } else { |
| // Override the cause if the previous one is |
| // ClassNotFoundException. |
| if (this.cause instanceof ClassNotFoundException) { |
| this.cause = cause; |
| } |
| } |
| } |
| |
| private Throwable getCause() { |
| return cause; |
| } |
| |
| @Override |
| public String toString() { |
| String l = locale.toString(); |
| if (l.isEmpty()) { |
| if (!locale.getVariant().isEmpty()) { |
| l = "__" + locale.getVariant(); |
| } else { |
| l = "\"\""; |
| } |
| } |
| return "CacheKey[" + name + ", lc=" + l + ")]"; |
| } |
| } |
| } |