mirror of
https://github.com/openjdk/jdk.git
synced 2025-08-28 15:24:43 +02:00
532 lines
20 KiB
Java
532 lines
20 KiB
Java
/*
|
|
* 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 + ")]";
|
|
}
|
|
}
|
|
}
|