/*
* 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 /** initial size of the bundle cache */
private static final int INITIAL_CACHE_SIZE = 32;
constant indicating that no resource bundle exists /** 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.
/**
* 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.
/**
* 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.
/**
* 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
/**
* 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.
Params: - cacheKey – the key to look up the cache
Returns: the ResourceBundle found in the cache or null
/**
* 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.
Params: - cacheKey – the key for the resource bundle
- bundle – the resource bundle to be put in the cache
Returns: the ResourceBundle for the cacheKey; if someone has put
the bundle before this call, the one found in the cache is
returned.
/**
* 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.
/**
* 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.
/**
* 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.
/**
* 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.
/**
* 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.
/**
* 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.
/**
* 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.
/**
* 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 + ")]";
}
}
}