ResourceBundleWrapper.java

// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 ******************************************************************************
 * Copyright (C) 2004-2016, International Business Machines Corporation and
 * others. All Rights Reserved.
 ******************************************************************************
 */

package com.ibm.icu.impl;

import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.MissingResourceException;
import java.util.Objects;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;

/**
 * just a wrapper for Java ListResourceBundles and
 *
 * @author ram
 */
public final class ResourceBundleWrapper extends UResourceBundle {
    private ResourceBundle bundle = null;
    private String localeID = null;
    private String baseName = null;
    private List<String> keys = null;

    /** Loader for bundle instances, for caching. */
    private abstract static class Loader {
        abstract ResourceBundleWrapper load();
    }

    private static class BundleCacheKey {
        public final String baseName;
        public final ClassLoader classLoader;

        private BundleCacheKey(String baseName, ClassLoader classLoader) {
            this.baseName = baseName;
            this.classLoader = classLoader;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            BundleCacheKey that = (BundleCacheKey) o;
            return Objects.equals(baseName, that.baseName)
                    && Objects.equals(classLoader, that.classLoader);
        }

        @Override
        public int hashCode() {
            return Objects.hash(baseName, classLoader);
        }
    }

    private static CacheBase<BundleCacheKey, ResourceBundleWrapper, Loader> BUNDLE_CACHE =
            new SoftCache<BundleCacheKey, ResourceBundleWrapper, Loader>() {
                @Override
                protected ResourceBundleWrapper createInstance(
                        BundleCacheKey unusedKey, Loader loader) {
                    return loader.load();
                }
            };

    private ResourceBundleWrapper(ResourceBundle bundle) {
        this.bundle = bundle;
    }

    @Override
    protected Object handleGetObject(String aKey) {
        ResourceBundleWrapper current = this;
        Object obj = null;
        while (current != null) {
            try {
                obj = current.bundle.getObject(aKey);
                break;
            } catch (MissingResourceException ex) {
                current = (ResourceBundleWrapper) current.getParent();
            }
        }
        if (obj == null) {
            throw new MissingResourceException(
                    "Can't find resource for bundle " + baseName + ", key " + aKey,
                    this.getClass().getName(),
                    aKey);
        }
        return obj;
    }

    @Override
    public Enumeration<String> getKeys() {
        return Collections.enumeration(keys);
    }

    private void initKeysVector() {
        ResourceBundleWrapper current = this;
        keys = new ArrayList<String>();
        while (current != null) {
            Enumeration<String> e = current.bundle.getKeys();
            while (e.hasMoreElements()) {
                String elem = e.nextElement();
                if (!keys.contains(elem)) {
                    keys.add(elem);
                }
            }
            current = (ResourceBundleWrapper) current.getParent();
        }
    }

    @Override
    protected String getLocaleID() {
        return localeID;
    }

    @Override
    protected String getBaseName() {
        return bundle.getClass().getName().replace('.', '/');
    }

    @Override
    public ULocale getULocale() {
        return new ULocale(localeID);
    }

    @Override
    public UResourceBundle getParent() {
        return (UResourceBundle) parent;
    }

    // Flag for enabling/disabling debugging code
    private static final boolean DEBUG = ICUDebug.enabled("resourceBundleWrapper");

    // This method is for super class's instantiateBundle method
    public static ResourceBundleWrapper getBundleInstance(
            String baseName, String localeID, ClassLoader root, boolean disableFallback) {
        if (root == null) {
            root = ClassLoaderUtil.getClassLoader();
        }
        ResourceBundleWrapper b;
        if (disableFallback) {
            b = instantiateBundle(baseName, localeID, null, root, disableFallback);
        } else {
            b =
                    instantiateBundle(
                            baseName,
                            localeID,
                            ULocale.getDefault().getBaseName(),
                            root,
                            disableFallback);
        }
        if (b == null) {
            String separator = "_";
            if (baseName.indexOf('/') >= 0) {
                separator = "/";
            }
            throw new MissingResourceException(
                    "Could not find the bundle " + baseName + separator + localeID, "", "");
        }
        return b;
    }

    private static boolean localeIDStartsWithLangSubtag(String localeID, String lang) {
        return localeID.startsWith(lang)
                && (localeID.length() == lang.length() || localeID.charAt(lang.length()) == '_');
    }

    private static ResourceBundleWrapper instantiateBundle(
            final String baseName,
            final String localeID,
            final String defaultID,
            final ClassLoader root,
            final boolean disableFallback) {
        final String name = localeID.isEmpty() ? baseName : baseName + '_' + localeID;
        BundleCacheKey cacheKey =
                new BundleCacheKey(disableFallback ? name : name + '#' + defaultID, root);
        return BUNDLE_CACHE.getInstance(
                cacheKey,
                new Loader() {
                    @Override
                    public ResourceBundleWrapper load() {
                        ResourceBundleWrapper parent = null;
                        int i = localeID.lastIndexOf('_');

                        boolean loadFromProperties = false;
                        boolean parentIsRoot = false;
                        if (i != -1) {
                            String locName = localeID.substring(0, i);
                            parent =
                                    instantiateBundle(
                                            baseName, locName, defaultID, root, disableFallback);
                        } else if (!localeID.isEmpty()) {
                            parent =
                                    instantiateBundle(
                                            baseName, "", defaultID, root, disableFallback);
                            parentIsRoot = true;
                        }
                        ResourceBundleWrapper b = null;
                        try {
                            Class<? extends ResourceBundle> cls =
                                    root.loadClass(name).asSubclass(ResourceBundle.class);
                            ResourceBundle bx = cls.newInstance();
                            b = new ResourceBundleWrapper(bx);
                            if (parent != null) {
                                b.setParent(parent);
                            }
                            b.baseName = baseName;
                            b.localeID = localeID;
                        } catch (ClassNotFoundException e) {
                            loadFromProperties = true;
                        } catch (NoClassDefFoundError e) {
                            loadFromProperties = true;
                        } catch (Exception e) {
                            if (DEBUG) System.out.println("failure");
                            if (DEBUG) System.out.println(e);
                        }

                        if (loadFromProperties) {
                            try {
                                final String resName = name.replace('.', '/') + ".properties";
                                InputStream stream =
                                        java.security.AccessController.doPrivileged(
                                                new java.security.PrivilegedAction<InputStream>() {
                                                    @Override
                                                    public InputStream run() {
                                                        return root.getResourceAsStream(resName);
                                                    }
                                                });
                                if (stream != null) {
                                    // make sure it is buffered
                                    stream = new java.io.BufferedInputStream(stream);
                                    try {
                                        b =
                                                new ResourceBundleWrapper(
                                                        new PropertyResourceBundle(stream));
                                        if (parent != null) {
                                            b.setParent(parent);
                                        }
                                        b.baseName = baseName;
                                        b.localeID = localeID;
                                    } catch (Exception ex) {
                                        // throw away exception
                                    } finally {
                                        try {
                                            stream.close();
                                        } catch (Exception ex) {
                                            // throw away exception
                                        }
                                    }
                                }

                                // if a bogus locale is passed then the parent should be
                                // the default locale not the root locale!
                                if (b == null
                                        && !disableFallback
                                        && !localeID.isEmpty()
                                        && localeID.indexOf('_') < 0
                                        && !localeIDStartsWithLangSubtag(defaultID, localeID)) {
                                    // localeID is only a language subtag, different from the
                                    // default language.
                                    b =
                                            instantiateBundle(
                                                    baseName,
                                                    defaultID,
                                                    defaultID,
                                                    root,
                                                    disableFallback);
                                }
                                // if still could not find the bundle then return the parent
                                if (b == null && (!parentIsRoot || !disableFallback)) {
                                    b = parent;
                                }
                            } catch (Exception e) {
                                if (DEBUG) System.out.println("failure");
                                if (DEBUG) System.out.println(e);
                            }
                        }
                        if (b != null) {
                            b.initKeysVector();
                        } else {
                            if (DEBUG)
                                System.out.println(
                                        "Returning null for " + baseName + "_" + localeID);
                        }
                        return b;
                    }
                });
    }
}