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