Path: blob/master/src/java.base/share/classes/sun/util/resources/Bundles.java
41159 views
/*1* Copyright (c) 2015, 2021, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425/*26* (C) Copyright Taligent, Inc. 1996, 1997 - All Rights Reserved27* (C) Copyright IBM Corp. 1996 - 1999 - All Rights Reserved28*29* The original version of this source code and documentation30* is copyrighted and owned by Taligent, Inc., a wholly-owned31* subsidiary of IBM. These materials are provided under terms32* of a License Agreement between Taligent and Sun. This technology33* is protected by multiple US and International patents.34*35* This notice and attribution to Taligent may not be removed.36* Taligent is a registered trademark of Taligent, Inc.37*38*/3940package sun.util.resources;4142import java.lang.ref.ReferenceQueue;43import java.lang.ref.SoftReference;44import java.security.AccessController;45import java.security.PrivilegedAction;46import java.util.Enumeration;47import java.util.Iterator;48import java.util.List;49import java.util.Locale;50import java.util.MissingResourceException;51import java.util.Objects;52import java.util.ResourceBundle;53import java.util.ServiceConfigurationError;54import java.util.ServiceLoader;55import java.util.concurrent.ConcurrentHashMap;56import java.util.concurrent.ConcurrentMap;57import java.util.spi.ResourceBundleProvider;58import jdk.internal.access.JavaUtilResourceBundleAccess;59import jdk.internal.access.SharedSecrets;6061/**62*/63public abstract class Bundles {6465/** initial size of the bundle cache */66private static final int INITIAL_CACHE_SIZE = 32;6768/** constant indicating that no resource bundle exists */69private static final ResourceBundle NONEXISTENT_BUNDLE = new ResourceBundle() {70@Override71public Enumeration<String> getKeys() { return null; }72@Override73protected Object handleGetObject(String key) { return null; }74@Override75public String toString() { return "NONEXISTENT_BUNDLE"; }76};7778private static final JavaUtilResourceBundleAccess bundleAccess79= SharedSecrets.getJavaUtilResourceBundleAccess();8081/**82* The cache is a map from cache keys (with bundle base name, locale, and83* class loader) to either a resource bundle or NONEXISTENT_BUNDLE wrapped by a84* BundleReference.85*86* The cache is a ConcurrentMap, allowing the cache to be searched87* concurrently by multiple threads. This will also allow the cache keys88* to be reclaimed along with the ClassLoaders they reference.89*90* This variable would be better named "cache", but we keep the old91* name for compatibility with some workarounds for bug 4212439.92*/93private static final ConcurrentMap<CacheKey, BundleReference> cacheList94= new ConcurrentHashMap<>(INITIAL_CACHE_SIZE);9596/**97* Queue for reference objects referring to class loaders or bundles.98*/99private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();100101private Bundles() {102}103104public static ResourceBundle of(String baseName, Locale locale, Strategy strategy) {105return loadBundleOf(baseName, locale, strategy);106}107108private static ResourceBundle loadBundleOf(String baseName,109Locale targetLocale,110Strategy strategy) {111Objects.requireNonNull(baseName);112Objects.requireNonNull(targetLocale);113Objects.requireNonNull(strategy);114115CacheKey cacheKey = new CacheKey(baseName, targetLocale);116117ResourceBundle bundle = null;118119// Quick lookup of the cache.120BundleReference bundleRef = cacheList.get(cacheKey);121if (bundleRef != null) {122bundle = bundleRef.get();123}124125// If this bundle and all of its parents are valid,126// then return this bundle.127if (isValidBundle(bundle)) {128return bundle;129}130131// Get the providers for loading the "leaf" bundle (i.e., bundle for132// targetLocale). If no providers are required for the bundle,133// none of its parents will require providers.134Class<? extends ResourceBundleProvider> type135= strategy.getResourceBundleProviderType(baseName, targetLocale);136if (type != null) {137@SuppressWarnings("unchecked")138ServiceLoader<ResourceBundleProvider> providers139= (ServiceLoader<ResourceBundleProvider>) ServiceLoader.loadInstalled(type);140cacheKey.setProviders(providers);141}142143List<Locale> candidateLocales = strategy.getCandidateLocales(baseName, targetLocale);144bundle = findBundleOf(cacheKey, strategy, baseName, candidateLocales, 0);145if (bundle == null) {146throwMissingResourceException(baseName, targetLocale, cacheKey.getCause());147}148return bundle;149}150151private static ResourceBundle findBundleOf(CacheKey cacheKey,152Strategy strategy,153String baseName,154List<Locale> candidateLocales,155int index) {156ResourceBundle parent = null;157Locale targetLocale = candidateLocales.get(index);158if (index != candidateLocales.size() - 1) {159parent = findBundleOf(cacheKey, strategy, baseName, candidateLocales, index + 1);160}161162// Before we do the real loading work, see whether we need to163// do some housekeeping: If resource bundles have been nulled out,164// remove all related information from the cache.165cleanupCache();166167// find an individual ResourceBundle in the cache168cacheKey.setLocale(targetLocale);169ResourceBundle bundle = findBundleInCache(cacheKey);170if (bundle != null) {171if (bundle == NONEXISTENT_BUNDLE) {172return parent;173}174if (bundleAccess.getParent(bundle) == parent) {175return bundle;176}177// Remove bundle from the cache.178BundleReference bundleRef = cacheList.get(cacheKey);179if (bundleRef != null && bundleRef.get() == bundle) {180cacheList.remove(cacheKey, bundleRef);181}182}183184// Determine if providers should be used for loading the bundle.185// An assumption here is that if the leaf bundle of a look-up path is186// in java.base, all bundles of the path are in java.base.187// (e.g., en_US of path en_US -> en -> root is in java.base and the rest188// are in java.base as well)189// This assumption isn't valid for general bundle loading.190ServiceLoader<ResourceBundleProvider> providers = cacheKey.getProviders();191if (providers != null) {192if (strategy.getResourceBundleProviderType(baseName, targetLocale) == null) {193providers = null;194}195}196197CacheKey constKey = (CacheKey) cacheKey.clone();198try {199if (providers != null) {200bundle = loadBundleFromProviders(baseName, targetLocale, providers, cacheKey);201} else {202try {203String bundleName = strategy.toBundleName(baseName, targetLocale);204Class<?> c = Class.forName(Bundles.class.getModule(), bundleName);205if (c != null && ResourceBundle.class.isAssignableFrom(c)) {206@SuppressWarnings("unchecked")207Class<ResourceBundle> bundleClass = (Class<ResourceBundle>) c;208bundle = bundleAccess.newResourceBundle(bundleClass);209}210if (bundle == null) {211var otherBundleName = toOtherBundleName(baseName, bundleName, targetLocale);212if (!bundleName.equals(otherBundleName)) {213c = Class.forName(Bundles.class.getModule(), otherBundleName);214if (c != null && ResourceBundle.class.isAssignableFrom(c)) {215@SuppressWarnings("unchecked")216Class<ResourceBundle> bundleClass = (Class<ResourceBundle>) c;217bundle = bundleAccess.newResourceBundle(bundleClass);218}219}220}221} catch (Exception e) {222cacheKey.setCause(e);223}224}225} finally {226if (constKey.getCause() instanceof InterruptedException) {227Thread.currentThread().interrupt();228}229}230231if (bundle == null) {232// Put NONEXISTENT_BUNDLE in the cache as a mark that there's no bundle233// instance for the locale.234putBundleInCache(cacheKey, NONEXISTENT_BUNDLE);235return parent;236}237238if (parent != null && bundleAccess.getParent(bundle) == null) {239bundleAccess.setParent(bundle, parent);240}241bundleAccess.setLocale(bundle, targetLocale);242bundleAccess.setName(bundle, baseName);243bundle = putBundleInCache(cacheKey, bundle);244return bundle;245}246247private static void cleanupCache() {248Object ref;249while ((ref = referenceQueue.poll()) != null) {250cacheList.remove(((CacheKeyReference)ref).getCacheKey());251}252}253254/**255* Loads ResourceBundle from service providers.256*/257@SuppressWarnings("removal")258private static ResourceBundle loadBundleFromProviders(String baseName,259Locale locale,260ServiceLoader<ResourceBundleProvider> providers,261CacheKey cacheKey)262{263return AccessController.doPrivileged(264new PrivilegedAction<>() {265public ResourceBundle run() {266for (Iterator<ResourceBundleProvider> itr = providers.iterator(); itr.hasNext(); ) {267try {268ResourceBundleProvider provider = itr.next();269ResourceBundle bundle = provider.getBundle(baseName, locale);270if (bundle != null) {271return bundle;272}273} catch (ServiceConfigurationError | SecurityException e) {274if (cacheKey != null) {275cacheKey.setCause(e);276}277}278}279return null;280}281});282283}284285private static boolean isValidBundle(ResourceBundle bundle) {286return bundle != null && bundle != NONEXISTENT_BUNDLE;287}288289/**290* Throw a MissingResourceException with proper message291*/292private static void throwMissingResourceException(String baseName,293Locale locale,294Throwable cause) {295// If the cause is a MissingResourceException, avoid creating296// a long chain. (6355009)297if (cause instanceof MissingResourceException) {298cause = null;299}300MissingResourceException e;301e = new MissingResourceException("Can't find bundle for base name "302+ baseName + ", locale " + locale,303baseName + "_" + locale, // className304"");305e.initCause(cause);306throw e;307}308309/**310* Finds a bundle in the cache.311*312* @param cacheKey the key to look up the cache313* @return the ResourceBundle found in the cache or null314*/315private static ResourceBundle findBundleInCache(CacheKey cacheKey) {316BundleReference bundleRef = cacheList.get(cacheKey);317if (bundleRef == null) {318return null;319}320return bundleRef.get();321}322323/**324* Put a new bundle in the cache.325*326* @param cacheKey the key for the resource bundle327* @param bundle the resource bundle to be put in the cache328* @return the ResourceBundle for the cacheKey; if someone has put329* the bundle before this call, the one found in the cache is330* returned.331*/332private static ResourceBundle putBundleInCache(CacheKey cacheKey,333ResourceBundle bundle) {334CacheKey key = (CacheKey) cacheKey.clone();335BundleReference bundleRef = new BundleReference(bundle, referenceQueue, key);336337// Put the bundle in the cache if it's not been in the cache.338BundleReference result = cacheList.putIfAbsent(key, bundleRef);339340// If someone else has put the same bundle in the cache before341// us, we should use the one in the cache.342if (result != null) {343ResourceBundle rb = result.get();344if (rb != null) {345// Clear the back link to the cache key346bundle = rb;347// Clear the reference in the BundleReference so that348// it won't be enqueued.349bundleRef.clear();350} else {351// Replace the invalid (garbage collected)352// instance with the valid one.353cacheList.put(key, bundleRef);354}355}356return bundle;357}358359/**360* Generates the other bundle name for languages that have changed,361* i.e. "he", "id", and "yi"362*363* @param baseName ResourceBundle base name364* @param bundleName ResourceBundle bundle name365* @param locale locale366* @return the other bundle name, or the same name for non-legacy ISO languages367*/368public static String toOtherBundleName(String baseName, String bundleName, Locale locale) {369var simpleName= baseName.substring(baseName.lastIndexOf('.') + 1);370var suffix = bundleName.substring(bundleName.lastIndexOf(simpleName) + simpleName.length());371var otherSuffix = switch(locale.getLanguage()) {372case "he" -> suffix.replaceFirst("^_he(_.*)?$", "_iw$1");373case "id" -> suffix.replaceFirst("^_id(_.*)?$", "_in$1");374case "yi" -> suffix.replaceFirst("^_yi(_.*)?$", "_ji$1");375case "iw" -> suffix.replaceFirst("^_iw(_.*)?$", "_he$1");376case "in" -> suffix.replaceFirst("^_in(_.*)?$", "_id$1");377case "ji" -> suffix.replaceFirst("^_ji(_.*)?$", "_yi$1");378default -> suffix;379};380381if (suffix.equals(otherSuffix)) {382return bundleName;383} else {384return bundleName.substring(0, bundleName.lastIndexOf(suffix)) + otherSuffix;385}386}387388/**389* The Strategy interface defines methods that are called by Bundles.of during390* the resource bundle loading process.391*/392public interface Strategy {393/**394* Returns a list of locales to be looked up for bundle loading.395*/396List<Locale> getCandidateLocales(String baseName, Locale locale);397398/**399* Returns the bundle name for the given baseName and locale.400*/401String toBundleName(String baseName, Locale locale);402403/**404* Returns the service provider type for the given baseName405* and locale, or null if no service providers should be used.406*/407Class<? extends ResourceBundleProvider> getResourceBundleProviderType(String baseName,408Locale locale);409}410411/**412* The common interface to get a CacheKey in LoaderReference and413* BundleReference.414*/415private static interface CacheKeyReference {416CacheKey getCacheKey();417}418419/**420* References to bundles are soft references so that they can be garbage421* collected when they have no hard references.422*/423private static class BundleReference extends SoftReference<ResourceBundle>424implements CacheKeyReference {425private final CacheKey cacheKey;426427BundleReference(ResourceBundle referent, ReferenceQueue<Object> q, CacheKey key) {428super(referent, q);429cacheKey = key;430}431432@Override433public CacheKey getCacheKey() {434return cacheKey;435}436}437438/**439* Key used for cached resource bundles. The key checks the base440* name, the locale, and the class loader to determine if the441* resource is a match to the requested one. The loader may be442* null, but the base name and the locale must have a non-null443* value.444*/445private static class CacheKey implements Cloneable {446// These two are the actual keys for lookup in Map.447private String name;448private Locale locale;449450// Placeholder for an error report by a Throwable451private Throwable cause;452453// Hash code value cache to avoid recalculating the hash code454// of this instance.455private int hashCodeCache;456457// The service loader to load bundles or null if no service loader458// is required.459private ServiceLoader<ResourceBundleProvider> providers;460461CacheKey(String baseName, Locale locale) {462this.name = baseName;463this.locale = locale;464calculateHashCode();465}466467String getName() {468return name;469}470471CacheKey setName(String baseName) {472if (!this.name.equals(baseName)) {473this.name = baseName;474calculateHashCode();475}476return this;477}478479Locale getLocale() {480return locale;481}482483CacheKey setLocale(Locale locale) {484if (!this.locale.equals(locale)) {485this.locale = locale;486calculateHashCode();487}488return this;489}490491ServiceLoader<ResourceBundleProvider> getProviders() {492return providers;493}494495void setProviders(ServiceLoader<ResourceBundleProvider> providers) {496this.providers = providers;497}498499@Override500public boolean equals(Object other) {501if (this == other) {502return true;503}504try {505final CacheKey otherEntry = (CacheKey)other;506//quick check to see if they are not equal507if (hashCodeCache != otherEntry.hashCodeCache) {508return false;509}510return locale.equals(otherEntry.locale)511&& name.equals(otherEntry.name);512} catch (NullPointerException | ClassCastException e) {513}514return false;515}516517@Override518public int hashCode() {519return hashCodeCache;520}521522private void calculateHashCode() {523hashCodeCache = name.hashCode() << 3;524hashCodeCache ^= locale.hashCode();525}526527@Override528public Object clone() {529try {530CacheKey clone = (CacheKey) super.clone();531// Clear the reference to a Throwable532clone.cause = null;533// Clear the reference to a ServiceLoader534clone.providers = null;535return clone;536} catch (CloneNotSupportedException e) {537//this should never happen538throw new InternalError(e);539}540}541542private void setCause(Throwable cause) {543if (this.cause == null) {544this.cause = cause;545} else {546// Override the cause if the previous one is547// ClassNotFoundException.548if (this.cause instanceof ClassNotFoundException) {549this.cause = cause;550}551}552}553554private Throwable getCause() {555return cause;556}557558@Override559public String toString() {560String l = locale.toString();561if (l.isEmpty()) {562if (!locale.getVariant().isEmpty()) {563l = "__" + locale.getVariant();564} else {565l = "\"\"";566}567}568return "CacheKey[" + name + ", lc=" + l + ")]";569}570}571}572573574