package java.util;

import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.jar.JarEntry;
import java.util.spi.ResourceBundleControlProvider;

import sun.reflect.CallerSensitive;
import sun.reflect.Reflection;
import sun.util.locale.BaseLocale;
import sun.util.locale.LocaleObjectCache;

//解决本地化,国际化类
public abstract class ResourceBundle {

    //包缓存的初始大小
    private static final int INITIAL_CACHE_SIZE = 32;

    //不存在资源束的常量
    private static final ResourceBundle NONEXISTENT_BUNDLE = new ResourceBundle() {
        public Enumeration<String> getKeys(){ return null; }

        protected Object handleGetObject(String key){ return null; }

        public String toString(){ return "NONEXISTENT_BUNDLE"; }
    };


    //缓存是ConcurrentHashMap
    private static final ConcurrentMap<CacheKey, BundleReference> cacheList
            = new ConcurrentHashMap<>(INITIAL_CACHE_SIZE);

    //    引用类装入器或包的引用对象的队列
    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();

    public String getBaseBundleName(){
        return name;
    }
    protected ResourceBundle parent = null;
    private Locale locale = null; //本地
    private String name;
    private volatile boolean expired;
    private volatile CacheKey cacheKey;
    private volatile Set<String> keySet;

    private static final List<ResourceBundleControlProvider> providers;

    static{
        List<ResourceBundleControlProvider> list = null;
        ServiceLoader<ResourceBundleControlProvider> serviceLoaders
                = ServiceLoader.loadInstalled(ResourceBundleControlProvider.class);
        for(ResourceBundleControlProvider provider : serviceLoaders){
            if(list == null){
                list = new ArrayList<>();
            }
            list.add(provider);
        }
        providers = list;
    }

    public ResourceBundle(){
    }
    public final String getString(String key){
        return (String) getObject(key);
    }
    public final String[] getStringArray(String key){
        return (String[]) getObject(key);
    }
    public final Object getObject(String key){
        Object obj = handleGetObject(key);
        if(obj == null){
            if(parent != null){
                obj = parent.getObject(key);
            }
            if(obj == null){
                throw new MissingResourceException("Can't find resource for bundle "
                                                   + this.getClass().getName()
                                                   + ", key " + key,
                                                   this.getClass().getName(),
                                                   key);
            }
        }
        return obj;
    }
    public Locale getLocale(){
        return locale;
    }
    private static ClassLoader getLoader(Class<?> caller){
        ClassLoader cl = caller == null ? null : caller.getClassLoader();
        if(cl == null){
            // When the caller's loader is the boot class loader, cl is null
            // here. In that case, ClassLoader.getSystemClassLoader() may
            // return the same class loader that the application is
            // using. We therefore use a wrapper ClassLoader to create a
            // separate scope for bundles loaded on behalf of the Java
            // runtime so that these bundles cannot be returned from the
            // cache to the application (5048280).
            cl = RBClassLoader.INSTANCE;
        }
        return cl;
    }
private static class RBClassLoader extends ClassLoader {
        private static final RBClassLoader INSTANCE = AccessController.doPrivileged(
                new PrivilegedAction<RBClassLoader>() {
                    public RBClassLoader run(){
                        return new RBClassLoader();
                    }
                });
        private static final ClassLoader loader;

        static{
            // Find the extension class loader.
            ClassLoader ld = ClassLoader.getSystemClassLoader();
            ClassLoader parent;
            while ((parent = ld.getParent()) != null) {
                ld = parent;
            }
            loader = ld;
        }

        private RBClassLoader(){
        }

        public Class<?> loadClass(String name) throws ClassNotFoundException{
            if(loader != null){
                return loader.loadClass(name);
            }
            return Class.forName(name);
        }

        public URL getResource(String name){
            if(loader != null){
                return loader.getResource(name);
            }
            return ClassLoader.getSystemResource(name);
        }

        public InputStream getResourceAsStream(String name){
            if(loader != null){
                return loader.getResourceAsStream(name);
            }
            return ClassLoader.getSystemResourceAsStream(name);
        }
    }
    protected void setParent(ResourceBundle parent){
        assert parent != NONEXISTENT_BUNDLE;
        this.parent = parent;
    }
    private static class CacheKey implements Cloneable {
        // These three are the actual keys for lookup in Map.
        private String name;
        private Locale locale;
        private LoaderReference loaderRef;

        // bundle format which is necessary for calling
        // Control.needsReload().
        private String format;

        // These time values are in CacheKey so that NONEXISTENT_BUNDLE
        // doesn't need to be cloned for caching.

        // The time when the bundle has been loaded
        private volatile long loadTime;

        // The time when the bundle expires in the cache, or either
        // Control.TTL_DONT_CACHE or Control.TTL_NO_EXPIRATION_CONTROL.
        private volatile long expirationTime;

        // 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;

        CacheKey(String baseName, Locale locale, ClassLoader loader){
            this.name = baseName;
            this.locale = locale;
            if(loader == null){
                this.loaderRef = null;
            }else{
                loaderRef = new LoaderReference(loader, referenceQueue, this);
            }
            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;
        }

        ClassLoader getLoader(){
            return (loaderRef != null) ? loaderRef.get() : null;
        }

        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;
                }
                //are the names the same?
                if(!name.equals(otherEntry.name)){
                    return false;
                }
                // are the locales the same?
                if(!locale.equals(otherEntry.locale)){
                    return false;
                }
                //are refs (both non-null) or (both null)?
                if(loaderRef == null){
                    return otherEntry.loaderRef == null;
                }
                ClassLoader loader = loaderRef.get();
                return (otherEntry.loaderRef != null)
                       // with a null reference we can no longer find
                       // out which class loader was referenced; so
                       // treat it as unequal
                       && (loader != null)
                       && (loader == otherEntry.loaderRef.get());
            } catch (NullPointerException | ClassCastException e) {
            }
            return false;
        }

        public int hashCode(){
            return hashCodeCache;
        }

        private void calculateHashCode(){
            hashCodeCache = name.hashCode() << 3;
            hashCodeCache ^= locale.hashCode();
            ClassLoader loader = getLoader();
            if(loader != null){
                hashCodeCache ^= loader.hashCode();
            }
        }

        public Object clone(){
            try {
                CacheKey clone = (CacheKey) super.clone();
                if(loaderRef != null){
                    clone.loaderRef = new LoaderReference(loaderRef.get(),
                                                          referenceQueue, clone);
                }
                // Clear the reference to a Throwable
                clone.cause = null;
                return clone;
            } catch (CloneNotSupportedException e) {
                //this should never happen
                throw new InternalError(e);
            }
        }

        String getFormat(){
            return format;
        }

        void setFormat(String format){
            this.format = format;
        }

        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;
        }

        public String toString(){
            String l = locale.toString();
            if(l.length() == 0){
                if(locale.getVariant().length() != 0){
                    l = "__" + locale.getVariant();
                }else{
                    l = "\"\"";
                }
            }
            return "CacheKey[" + name + ", lc=" + l + ", ldr=" + getLoader()
                   + "(format=" + format + ")]";
        }
    }
    private static interface CacheKeyReference {
        public CacheKey getCacheKey();
    }
    private static class LoaderReference extends WeakReference<ClassLoader>
            implements CacheKeyReference {
        private CacheKey cacheKey;

        LoaderReference(ClassLoader referent, ReferenceQueue<Object> q, CacheKey key){
            super(referent, q);
            cacheKey = key;
        }

        public CacheKey getCacheKey(){
            return cacheKey;
        }
    }
    private static class BundleReference extends SoftReference<ResourceBundle>
            implements CacheKeyReference {
        private CacheKey cacheKey;

        BundleReference(ResourceBundle referent, ReferenceQueue<Object> q, CacheKey key){
            super(referent, q);
            cacheKey = key;
        }

        public CacheKey getCacheKey(){
            return cacheKey;
        }
    }
    @CallerSensitive
    public static final ResourceBundle getBundle(String baseName){
        return getBundleImpl(baseName, Locale.getDefault(),
                             getLoader(Reflection.getCallerClass()),
                             getDefaultControl(baseName));
    }
    @CallerSensitive
    public static final ResourceBundle getBundle(String baseName,
            Control control){
        return getBundleImpl(baseName, Locale.getDefault(),
                             getLoader(Reflection.getCallerClass()),
                             control);
    }
    @CallerSensitive
    public static final ResourceBundle getBundle(String baseName,
            Locale locale){
        return getBundleImpl(baseName, locale,
                             getLoader(Reflection.getCallerClass()),
                             getDefaultControl(baseName));
    }
    @CallerSensitive
    public static final ResourceBundle getBundle(String baseName, Locale targetLocale,
            Control control){
        return getBundleImpl(baseName, targetLocale,
                             getLoader(Reflection.getCallerClass()),
                             control);
    }
    public static ResourceBundle getBundle(String baseName, Locale locale,
            ClassLoader loader){
        if(loader == null){
            throw new NullPointerException();
        }
        return getBundleImpl(baseName, locale, loader, getDefaultControl(baseName));
    }
    public static ResourceBundle getBundle(String baseName, Locale targetLocale,
            ClassLoader loader, Control control){
        if(loader == null || control == null){
            throw new NullPointerException();
        }
        return getBundleImpl(baseName, targetLocale, loader, control);
    }

    private static Control getDefaultControl(String baseName){
        if(providers != null){
            for(ResourceBundleControlProvider provider : providers){
                Control control = provider.getControl(baseName);
                if(control != null){
                    return control;
                }
            }
        }
        return Control.INSTANCE;
    }

    private static ResourceBundle getBundleImpl(String baseName, Locale locale,
            ClassLoader loader, Control control){
        if(locale == null || control == null){
            throw new NullPointerException();
        }

        // We create a CacheKey here for use by this call. The base
        // name and loader will never change during the bundle loading
        // process. We have to make sure that the locale is set before
        // using it as a cache key.
        CacheKey cacheKey = new CacheKey(baseName, locale, loader);
        ResourceBundle bundle = null;

        // Quick lookup of the cache.
        BundleReference bundleRef = cacheList.get(cacheKey);
        if(bundleRef != null){
            bundle = bundleRef.get();
            bundleRef = null;
        }

        // If this bundle and all of its parents are valid (not expired),
        // then return this bundle. If any of the bundles is expired, we
        // don't call control.needsReload here but instead drop into the
        // complete loading process below.
        if(isValidBundle(bundle) && hasValidParentChain(bundle)){
            return bundle;
        }

        // No valid bundle was found in the cache, so we need to load the
        // resource bundle and its parents.

        boolean isKnownControl = (control == Control.INSTANCE) ||
                                 (control instanceof SingleFormatControl);
        List<String> formats = control.getFormats(baseName);
        if(!isKnownControl && !checkList(formats)){
            throw new IllegalArgumentException("Invalid Control: getFormats");
        }

        ResourceBundle baseBundle = null;
        for(Locale targetLocale = locale;
            targetLocale != null;
            targetLocale = control.getFallbackLocale(baseName, targetLocale)){
            List<Locale> candidateLocales = control.getCandidateLocales(baseName, targetLocale);
            if(!isKnownControl && !checkList(candidateLocales)){
                throw new IllegalArgumentException("Invalid Control: getCandidateLocales");
            }

            bundle = findBundle(cacheKey, candidateLocales, formats, 0, control, baseBundle);

            // If the loaded bundle is the base bundle and exactly for the
            // requested locale or the only candidate locale, then take the
            // bundle as the resulting one. If the loaded bundle is the base
            // bundle, it's put on hold until we finish processing all
            // fallback locales.
            if(isValidBundle(bundle)){
                boolean isBaseBundle = Locale.ROOT.equals(bundle.locale);
                if(!isBaseBundle || bundle.locale.equals(locale)
                   || (candidateLocales.size() == 1
                       && bundle.locale.equals(candidateLocales.get(0)))){
                    break;
                }

                // If the base bundle has been loaded, keep the reference in
                // baseBundle so that we can avoid any redundant loading in case
                // the control specify not to cache bundles.
                if(isBaseBundle && baseBundle == null){
                    baseBundle = bundle;
                }
            }
        }

        if(bundle == null){
            if(baseBundle == null){
                throwMissingResourceException(baseName, locale, cacheKey.getCause());
            }
            bundle = baseBundle;
        }

        keepAlive(loader);
        return bundle;
    }
    private static void keepAlive(ClassLoader loader){
        // Do nothing.
    }
    private static boolean checkList(List<?> a){
        boolean valid = (a != null && !a.isEmpty());
        if(valid){
            int size = a.size();
            for(int i = 0; valid && i < size; i++){
                valid = (a.get(i) != null);
            }
        }
        return valid;
    }

    private static ResourceBundle findBundle(CacheKey cacheKey,
            List<Locale> candidateLocales,
            List<String> formats,
            int index,
            Control control,
            ResourceBundle baseBundle){
        Locale targetLocale = candidateLocales.get(index);
        ResourceBundle parent = null;
        if(index != candidateLocales.size() - 1){
            parent = findBundle(cacheKey, candidateLocales, formats, index + 1,
                                control, baseBundle);
        }else if(baseBundle != null && Locale.ROOT.equals(targetLocale)){
            return baseBundle;
        }

        // Before we do the real loading work, see whether we need to
        // do some housekeeping: If references to class loaders or
        // resource bundles have been nulled out, remove all related
        // information from the cache.
        Object ref;
        while ((ref = referenceQueue.poll()) != null) {
            cacheList.remove(((CacheKeyReference) ref).getCacheKey());
        }

        // flag indicating the resource bundle has expired in the cache
        boolean expiredBundle = false;

        // First, look up the cache to see if it's in the cache, without
        // attempting to load bundle.
        cacheKey.setLocale(targetLocale);
        ResourceBundle bundle = findBundleInCache(cacheKey, control);
        if(isValidBundle(bundle)){
            expiredBundle = bundle.expired;
            if(!expiredBundle){
                // If its parent is the one asked for by the candidate
                // locales (the runtime lookup path), we can take the cached
                // one. (If it's not identical, then we'd have to check the
                // parent's parents to be consistent with what's been
                // requested.)
                if(bundle.parent == parent){
                    return bundle;
                }
                // Otherwise, remove the cached one since we can't keep
                // the same bundles having different parents.
                BundleReference bundleRef = cacheList.get(cacheKey);
                if(bundleRef != null && bundleRef.get() == bundle){
                    cacheList.remove(cacheKey, bundleRef);
                }
            }
        }

        if(bundle != NONEXISTENT_BUNDLE){
            CacheKey constKey = (CacheKey) cacheKey.clone();

            try {
                bundle = loadBundle(cacheKey, formats, control, expiredBundle);
                if(bundle != null){
                    if(bundle.parent == null){
                        bundle.setParent(parent);
                    }
                    bundle.locale = targetLocale;
                    bundle = putBundleInCache(cacheKey, bundle, control);
                    return bundle;
                }

                // Put NONEXISTENT_BUNDLE in the cache as a mark that there's no bundle
                // instance for the locale.
                putBundleInCache(cacheKey, NONEXISTENT_BUNDLE, control);
            } finally {
                if(constKey.getCause() instanceof InterruptedException){
                    Thread.currentThread().interrupt();
                }
            }
        }
        return parent;
    }

    private static ResourceBundle loadBundle(CacheKey cacheKey,
            List<String> formats,
            Control control,
            boolean reload){

        // Here we actually load the bundle in the order of formats
        // specified by the getFormats() value.
        Locale targetLocale = cacheKey.getLocale();

        ResourceBundle bundle = null;
        int size = formats.size();
        for(int i = 0; i < size; i++){
            String format = formats.get(i);
            try {
                bundle = control.newBundle(cacheKey.getName(), targetLocale, format,
                                           cacheKey.getLoader(), reload);
            } catch (LinkageError error) {
                // We need to handle the LinkageError case due to
                // inconsistent case-sensitivity in ClassLoader.
                // See 6572242 for details.
                cacheKey.setCause(error);
            } catch (Exception cause) {
                cacheKey.setCause(cause);
            }
            if(bundle != null){
                // Set the format in the cache key so that it can be
                // used when calling needsReload later.
                cacheKey.setFormat(format);
                bundle.name = cacheKey.getName();
                bundle.locale = targetLocale;
                // Bundle provider might reuse instances. So we should make
                // sure to clear the expired flag here.
                bundle.expired = false;
                break;
            }
        }

        return bundle;
    }

    private static boolean isValidBundle(ResourceBundle bundle){
        return bundle != null && bundle != NONEXISTENT_BUNDLE;
    }
    private static boolean hasValidParentChain(ResourceBundle bundle){
        long now = System.currentTimeMillis();
        while (bundle != null) {
            if(bundle.expired){
                return false;
            }
            CacheKey key = bundle.cacheKey;
            if(key != null){
                long expirationTime = key.expirationTime;
                if(expirationTime >= 0 && expirationTime <= now){
                    return false;
                }
            }
            bundle = bundle.parent;
        }
        return true;
    }
    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;
        }
        throw new MissingResourceException("Can't find bundle for base name "
                                           + baseName + ", locale " + locale,
                                           baseName + "_" + locale, // className
                                           "",                      // key
                                           cause);
    }
    private static ResourceBundle findBundleInCache(CacheKey cacheKey,
            Control control){
        BundleReference bundleRef = cacheList.get(cacheKey);
        if(bundleRef == null){
            return null;
        }
        ResourceBundle bundle = bundleRef.get();
        if(bundle == null){
            return null;
        }
        ResourceBundle p = bundle.parent;
        assert p != NONEXISTENT_BUNDLE;
        if(p != null && p.expired){
            assert bundle != NONEXISTENT_BUNDLE;
            bundle.expired = true;
            bundle.cacheKey = null;
            cacheList.remove(cacheKey, bundleRef);
            bundle = null;
        }else{
            CacheKey key = bundleRef.getCacheKey();
            long expirationTime = key.expirationTime;
            if(!bundle.expired && expirationTime >= 0 &&
               expirationTime <= System.currentTimeMillis()){
                // its TTL period has expired.
                if(bundle != NONEXISTENT_BUNDLE){
                    // Synchronize here to call needsReload to avoid
                    // redundant concurrent calls for the same bundle.
                    synchronized (bundle) {
                        expirationTime = key.expirationTime;
                        if(!bundle.expired && expirationTime >= 0 &&
                           expirationTime <= System.currentTimeMillis()){
                            try {
                                bundle.expired = control.needsReload(key.getName(),
                                                                     key.getLocale(),
                                                                     key.getFormat(),
                                                                     key.getLoader(),
                                                                     bundle,
                                                                     key.loadTime);
                            } catch (Exception e) {
                                cacheKey.setCause(e);
                            }
                            if(bundle.expired){
                                // If the bundle needs to be reloaded, then
                                // remove the bundle from the cache, but
                                // return the bundle with the expired flag
                                // on.
                                bundle.cacheKey = null;
                                cacheList.remove(cacheKey, bundleRef);
                            }else{
                                // Update the expiration control info. and reuse
                                // the same bundle instance
                                setExpirationTime(key, control);
                            }
                        }
                    }
                }else{
                    // We just remove NONEXISTENT_BUNDLE from the cache.
                    cacheList.remove(cacheKey, bundleRef);
                    bundle = null;
                }
            }
        }
        return bundle;
    }
    private static ResourceBundle putBundleInCache(CacheKey cacheKey,
            ResourceBundle bundle,
            Control control){
        setExpirationTime(cacheKey, control);
        if(cacheKey.expirationTime != Control.TTL_DONT_CACHE){
            CacheKey key = (CacheKey) cacheKey.clone();
            BundleReference bundleRef = new BundleReference(bundle, referenceQueue, key);
            bundle.cacheKey = 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 and it has not expired, we should use the one in the cache.
            if(result != null){
                ResourceBundle rb = result.get();
                if(rb != null && !rb.expired){
                    // Clear the back link to the cache key
                    bundle.cacheKey = null;
                    bundle = rb;
                    // Clear the reference in the BundleReference so that
                    // it won't be enqueued.
                    bundleRef.clear();
                }else{
                    // Replace the invalid (garbage collected or expired)
                    // instance with the valid one.
                    cacheList.put(key, bundleRef);
                }
            }
        }
        return bundle;
    }

    private static void setExpirationTime(CacheKey cacheKey, Control control){
        long ttl = control.getTimeToLive(cacheKey.getName(),
                                         cacheKey.getLocale());
        if(ttl >= 0){
            // If any expiration time is specified, set the time to be
            // expired in the cache.
            long now = System.currentTimeMillis();
            cacheKey.loadTime = now;
            cacheKey.expirationTime = now + ttl;
        }else if(ttl >= Control.TTL_NO_EXPIRATION_CONTROL){
            cacheKey.expirationTime = ttl;
        }else{
            throw new IllegalArgumentException("Invalid Control: TTL=" + ttl);
        }
    }
    @CallerSensitive
    public static final void clearCache(){
        clearCache(getLoader(Reflection.getCallerClass()));
    }
    public static final void clearCache(ClassLoader loader){
        if(loader == null){
            throw new NullPointerException();
        }
        Set<CacheKey> set = cacheList.keySet();
        for(CacheKey key : set){
            if(key.getLoader() == loader){
                set.remove(key);
            }
        }
    }
    protected abstract Object handleGetObject(String key);
    public abstract Enumeration<String> getKeys();
    public boolean containsKey(String key){
        if(key == null){
            throw new NullPointerException();
        }
        for(ResourceBundle rb = this; rb != null; rb = rb.parent){
            if(rb.handleKeySet().contains(key)){
                return true;
            }
        }
        return false;
    }
    public Set<String> keySet(){
        Set<String> keys = new HashSet<>();
        for(ResourceBundle rb = this; rb != null; rb = rb.parent){
            keys.addAll(rb.handleKeySet());
        }
        return keys;
    }
    protected Set<String> handleKeySet(){
        if(keySet == null){
            synchronized (this) {
                if(keySet == null){
                    Set<String> keys = new HashSet<>();
                    Enumeration<String> enumKeys = getKeys();
                    while (enumKeys.hasMoreElements()) {
                        String key = enumKeys.nextElement();
                        if(handleGetObject(key) != null){
                            keys.add(key);
                        }
                    }
                    keySet = keys;
                }
            }
        }
        return keySet;
    }
    public static class Control {
        /**
         * The default format <code>List</code>, which contains the strings
         * <code>"java.class"</code> and <code>"java.properties"</code>, in
         * this order. This <code>List</code> is {@linkplain
         * Collections#unmodifiableList(List) unmodifiable}.
         *
         * @see #getFormats(String)
         */
        public static final List<String> FORMAT_DEFAULT
                = Collections.unmodifiableList(Arrays.asList("java.class",
                                                             "java.properties"));

        /**
         * The class-only format <code>List</code> containing
         * <code>"java.class"</code>. This <code>List</code> is {@linkplain
         * Collections#unmodifiableList(List) unmodifiable}.
         *
         * @see #getFormats(String)
         */
        public static final List<String> FORMAT_CLASS
                = Collections.unmodifiableList(Arrays.asList("java.class"));

        /**
         * The properties-only format <code>List</code> containing
         * <code>"java.properties"</code>. This <code>List</code> is
         * {@linkplain Collections#unmodifiableList(List) unmodifiable}.
         *
         * @see #getFormats(String)
         */
        public static final List<String> FORMAT_PROPERTIES
                = Collections.unmodifiableList(Arrays.asList("java.properties"));

        /**
         * The time-to-live constant for not caching loaded resource bundle
         * instances.
         *
         * @see #getTimeToLive(String, Locale)
         */
        public static final long TTL_DONT_CACHE = -1;

        /**
         * The time-to-live constant for disabling the expiration control
         * for loaded resource bundle instances in the cache.
         *
         * @see #getTimeToLive(String, Locale)
         */
        public static final long TTL_NO_EXPIRATION_CONTROL = -2;

        private static final Control INSTANCE = new Control();

        /**
         * Sole constructor. (For invocation by subclass constructors,
         * typically implicit.)
         */
        protected Control(){
        }

        public static final Control getControl(List<String> formats){
            if(formats.equals(Control.FORMAT_PROPERTIES)){
                return SingleFormatControl.PROPERTIES_ONLY;
            }
            if(formats.equals(Control.FORMAT_CLASS)){
                return SingleFormatControl.CLASS_ONLY;
            }
            if(formats.equals(Control.FORMAT_DEFAULT)){
                return Control.INSTANCE;
            }
            throw new IllegalArgumentException();
        }

        public static final Control getNoFallbackControl(List<String> formats){
            if(formats.equals(Control.FORMAT_DEFAULT)){
                return NoFallbackControl.NO_FALLBACK;
            }
            if(formats.equals(Control.FORMAT_PROPERTIES)){
                return NoFallbackControl.PROPERTIES_ONLY_NO_FALLBACK;
            }
            if(formats.equals(Control.FORMAT_CLASS)){
                return NoFallbackControl.CLASS_ONLY_NO_FALLBACK;
            }
            throw new IllegalArgumentException();
        }

        public List<String> getFormats(String baseName){
            if(baseName == null){
                throw new NullPointerException();
            }
            return FORMAT_DEFAULT;
        }

        public List<Locale> getCandidateLocales(String baseName, Locale locale){
            if(baseName == null){
                throw new NullPointerException();
            }
            return new ArrayList<>(CANDIDATES_CACHE.get(locale.getBaseLocale()));
        }

        private static final CandidateListCache CANDIDATES_CACHE = new CandidateListCache();

        private static class CandidateListCache extends LocaleObjectCache<BaseLocale, List<Locale>> {
            protected List<Locale> createObject(BaseLocale base){
                String language = base.getLanguage();
                String script = base.getScript();
                String region = base.getRegion();
                String variant = base.getVariant();

                // Special handling for Norwegian
                boolean isNorwegianBokmal = false;
                boolean isNorwegianNynorsk = false;
                if(language.equals("no")){
                    if(region.equals("NO") && variant.equals("NY")){
                        variant = "";
                        isNorwegianNynorsk = true;
                    }else{
                        isNorwegianBokmal = true;
                    }
                }
                if(language.equals("nb") || isNorwegianBokmal){
                    List<Locale> tmpList = getDefaultList("nb", script, region, variant);
                    // Insert a locale replacing "nb" with "no" for every list entry
                    List<Locale> bokmalList = new LinkedList<>();
                    for(Locale l : tmpList){
                        bokmalList.add(l);
                        if(l.getLanguage().length() == 0){
                            break;
                        }
                        bokmalList.add(Locale.getInstance("no", l.getScript(), l.getCountry(),
                                                          l.getVariant(), null));
                    }
                    return bokmalList;
                }else if(language.equals("nn") || isNorwegianNynorsk){
                    // Insert no_NO_NY, no_NO, no after nn
                    List<Locale> nynorskList = getDefaultList("nn", script, region, variant);
                    int idx = nynorskList.size() - 1;
                    nynorskList.add(idx++, Locale.getInstance("no", "NO", "NY"));
                    nynorskList.add(idx++, Locale.getInstance("no", "NO", ""));
                    nynorskList.add(idx++, Locale.getInstance("no", "", ""));
                    return nynorskList;
                }
                // Special handling for Chinese
                else if(language.equals("zh")){
                    if(script.length() == 0 && region.length() > 0){
                        // Supply script for users who want to use zh_Hans/zh_Hant
                        // as bundle names (recommended for Java7+)
                        switch (region) {
                            case "TW":
                            case "HK":
                            case "MO":
                                script = "Hant";
                                break;
                            case "CN":
                            case "SG":
                                script = "Hans";
                                break;
                        }
                    }else if(script.length() > 0 && region.length() == 0){
                        // Supply region(country) for users who still package Chinese
                        // bundles using old convension.
                        switch (script) {
                            case "Hans":
                                region = "CN";
                                break;
                            case "Hant":
                                region = "TW";
                                break;
                        }
                    }
                }

                return getDefaultList(language, script, region, variant);
            }

            private static List<Locale> getDefaultList(String language, String script, String region, String variant){
                List<String> variants = null;

                if(variant.length() > 0){
                    variants = new LinkedList<>();
                    int idx = variant.length();
                    while (idx != -1) {
                        variants.add(variant.substring(0, idx));
                        idx = variant.lastIndexOf('_', --idx);
                    }
                }

                List<Locale> list = new LinkedList<>();

                if(variants != null){
                    for(String v : variants){
                        list.add(Locale.getInstance(language, script, region, v, null));
                    }
                }
                if(region.length() > 0){
                    list.add(Locale.getInstance(language, script, region, "", null));
                }
                if(script.length() > 0){
                    list.add(Locale.getInstance(language, script, "", "", null));

                    // With script, after truncating variant, region and script,
                    // start over without script.
                    if(variants != null){
                        for(String v : variants){
                            list.add(Locale.getInstance(language, "", region, v, null));
                        }
                    }
                    if(region.length() > 0){
                        list.add(Locale.getInstance(language, "", region, "", null));
                    }
                }
                if(language.length() > 0){
                    list.add(Locale.getInstance(language, "", "", "", null));
                }
                // Add root locale at the end
                list.add(Locale.ROOT);

                return list;
            }
        }

        public Locale getFallbackLocale(String baseName, Locale locale){
            if(baseName == null){
                throw new NullPointerException();
            }
            Locale defaultLocale = Locale.getDefault();
            return locale.equals(defaultLocale) ? null : defaultLocale;
        }

        public ResourceBundle newBundle(String baseName, Locale locale, String format,
                ClassLoader loader, boolean reload)
                throws IllegalAccessException, InstantiationException, IOException{
            String bundleName = toBundleName(baseName, locale);
            ResourceBundle bundle = null;
            if(format.equals("java.class")){
                try {
                    @SuppressWarnings("unchecked")
                    Class<? extends ResourceBundle> bundleClass
                            = (Class<? extends ResourceBundle>) loader.loadClass(bundleName);

                    // If the class isn't a ResourceBundle subclass, throw a
                    // ClassCastException.
                    if(ResourceBundle.class.isAssignableFrom(bundleClass)){
                        bundle = bundleClass.newInstance();
                    }else{
                        throw new ClassCastException(bundleClass.getName()
                                                     + " cannot be cast to ResourceBundle");
                    }
                } catch (ClassNotFoundException e) {
                }
            }else if(format.equals("java.properties")){
                final String resourceName = toResourceName0(bundleName, "properties");
                if(resourceName == null){
                    return bundle;
                }
                final ClassLoader classLoader = loader;
                final boolean reloadFlag = reload;
                InputStream stream = null;
                try {
                    stream = AccessController.doPrivileged(
                            new PrivilegedExceptionAction<InputStream>() {
                                public InputStream run() throws IOException{
                                    InputStream is = null;
                                    if(reloadFlag){
                                        URL url = classLoader.getResource(resourceName);
                                        if(url != null){
                                            URLConnection connection = url.openConnection();
                                            if(connection != null){
                                                // Disable caches to get fresh data for
                                                // reloading.
                                                connection.setUseCaches(false);
                                                is = connection.getInputStream();
                                            }
                                        }
                                    }else{
                                        is = classLoader.getResourceAsStream(resourceName);
                                    }
                                    return is;
                                }
                            });
                } catch (PrivilegedActionException e) {
                    throw (IOException) e.getException();
                }
                if(stream != null){
                    try {
                        bundle = new PropertyResourceBundle(stream);
                    } finally {
                        stream.close();
                    }
                }
            }else{
                throw new IllegalArgumentException("unknown format: " + format);
            }
            return bundle;
        }
        //ttl
        public long getTimeToLive(String baseName, Locale locale){
            if(baseName == null || locale == null){
                throw new NullPointerException();
            }
            return TTL_NO_EXPIRATION_CONTROL;
        }

        public boolean needsReload(String baseName, Locale locale,
                String format, ClassLoader loader,
                ResourceBundle bundle, long loadTime){
            if(bundle == null){
                throw new NullPointerException();
            }
            if(format.equals("java.class") || format.equals("java.properties")){
                format = format.substring(5);
            }
            boolean result = false;
            try {
                String resourceName = toResourceName0(toBundleName(baseName, locale), format);
                if(resourceName == null){
                    return result;
                }
                URL url = loader.getResource(resourceName);
                if(url != null){
                    long lastModified = 0;
                    URLConnection connection = url.openConnection();
                    if(connection != null){
                        // disable caches to get the correct data
                        connection.setUseCaches(false);
                        if(connection instanceof JarURLConnection){
                            JarEntry ent = ((JarURLConnection) connection).getJarEntry();
                            if(ent != null){
                                lastModified = ent.getTime();
                                if(lastModified == -1){
                                    lastModified = 0;
                                }
                            }
                        }else{
                            lastModified = connection.getLastModified();
                        }
                    }
                    result = lastModified >= loadTime;
                }
            } catch (NullPointerException npe) {
                throw npe;
            } catch (Exception e) {
                // ignore other exceptions
            }
            return result;
        }

        public String toBundleName(String baseName, Locale locale){
            if(locale == Locale.ROOT){
                return baseName;
            }

            String language = locale.getLanguage();
            String script = locale.getScript();
            String country = locale.getCountry();
            String variant = locale.getVariant();

            if(language == "" && country == "" && variant == ""){
                return baseName;
            }

            StringBuilder sb = new StringBuilder(baseName);
            sb.append('_');
            if(script != ""){
                if(variant != ""){
                    sb.append(language).append('_').append(script).append('_').append(country).append('_').append(variant);
                }else if(country != ""){
                    sb.append(language).append('_').append(script).append('_').append(country);
                }else{
                    sb.append(language).append('_').append(script);
                }
            }else{
                if(variant != ""){
                    sb.append(language).append('_').append(country).append('_').append(variant);
                }else if(country != ""){
                    sb.append(language).append('_').append(country);
                }else{
                    sb.append(language);
                }
            }
            return sb.toString();

        }

        /**
         * Converts the given <code>bundleName</code> to the form required
         * by the {@link ClassLoader#getResource ClassLoader.getResource}
         * method by replacing all occurrences of <code>'.'</code> in
         * <code>bundleName</code> with <code>'/'</code> and appending a
         * <code>'.'</code> and the given file <code>suffix</code>. For
         * example, if <code>bundleName</code> is
         * <code>"foo.bar.MyResources_ja_JP"</code> and <code>suffix</code>
         * is <code>"properties"</code>, then
         * <code>"foo/bar/MyResources_ja_JP.properties"</code> is returned.
         *
         * @param bundleName the bundle name
         * @param suffix     the file type suffix
         * @return the converted resource name
         * @throws NullPointerException if <code>bundleName</code> or <code>suffix</code>
         *                              is <code>null</code>
         */
        public final String toResourceName(String bundleName, String suffix){
            StringBuilder sb = new StringBuilder(bundleName.length() + 1 + suffix.length());
            sb.append(bundleName.replace('.', '/')).append('.').append(suffix);
            return sb.toString();
        }

        private String toResourceName0(String bundleName, String suffix){
            // application protocol check
            if(bundleName.contains("://")){
                return null;
            }else{
                return toResourceName(bundleName, suffix);
            }
        }
    }

    private static class SingleFormatControl extends Control {
        private static final Control PROPERTIES_ONLY
                = new SingleFormatControl(FORMAT_PROPERTIES);

        private static final Control CLASS_ONLY
                = new SingleFormatControl(FORMAT_CLASS);

        private final List<String> formats;

        protected SingleFormatControl(List<String> formats){
            this.formats = formats;
        }

        public List<String> getFormats(String baseName){
            if(baseName == null){
                throw new NullPointerException();
            }
            return formats;
        }
    }

    private static final class NoFallbackControl extends SingleFormatControl {
        private static final Control NO_FALLBACK
                = new NoFallbackControl(FORMAT_DEFAULT);

        private static final Control PROPERTIES_ONLY_NO_FALLBACK
                = new NoFallbackControl(FORMAT_PROPERTIES);

        private static final Control CLASS_ONLY_NO_FALLBACK
                = new NoFallbackControl(FORMAT_CLASS);

        protected NoFallbackControl(List<String> formats){
            super(formats);
        }

        public Locale getFallbackLocale(String baseName, Locale locale){
            if(baseName == null || locale == null){
                throw new NullPointerException();
            }
            return null;
        }
    }
}