/*
 * Copyright 2001-2004 The Apache Software Foundation.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.axis.i18n;

import org.apache.axis.components.logger.LogFactory;
import org.apache.commons.logging.Log;

import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

/**
 * <p>Wrapper class for resource bundles. Property files are used to store
 * resource strings, which are the only types of resources available.
 * Property files can inherit properties from other files so that
 * a base property file can be used and a small number of properties
 * can be over-ridden by another property file. For example you may
 * create an english version of a resource file named "resource.properties".
 * You then decide that the British English version of all of the properties
 * except one are the same, so there is no need to redefine all of the
 * properties in "resource_en_GB", just the one that is different.</p>
 * <p>The basename is the name of the property file without the ".properties"
 * extension.</p>
 * <p>Properties will be cached for performance.<p>
 * <p>Property values stored in the property files can also contain dynamic
 * variables. Any dynamic variable defined in PropertiesUtil.getVariableValue()
 * can be used (such as {date}), as well as arguments in the form {0}, {1}, etc.
 * Argument values are specified in the various overloaded getString() methods.</p>
 * 
 * @author Richard A. Sitze (rsitze@us.ibm.com)
 * @author Karl Moss (kmoss@macromedia.com)
 * @author Glen Daniels (gdaniels@apache.org)
 */
public class ProjectResourceBundle extends ResourceBundle {
    protected static Log log =
        LogFactory.getLog(ProjectResourceBundle.class.getName());


    // The static cache of ResourceBundles.
    // The key is the 'basename + locale + default locale'
    // The element is a ResourceBundle object
    private static final Hashtable bundleCache = new Hashtable();

    private static final Locale defaultLocale = Locale.getDefault();

    private final ResourceBundle resourceBundle;
    private final String resourceName;

    
    protected Object handleGetObject(String key)
        throws MissingResourceException
    {
        if (log.isDebugEnabled()) {
            log.debug(this.toString() + "::handleGetObject(" + key + ")");
        }
//            return resourceBundle.handleGetObject(key);
        Object obj;
        try {
            obj = resourceBundle.getObject(key);
        } catch (MissingResourceException e) {
            /* catch missing resource, ignore, & return null
             * if this method doesn't return null, then parents
             * are not searched
             */
            obj = null;
        }
        return obj;
    }
    
    public Enumeration getKeys() {
        Enumeration myKeys = resourceBundle.getKeys();
        if (parent == null) {
            return myKeys;
        } else {
            final HashSet set = new HashSet();
            while (myKeys.hasMoreElements()) {
                set.add(myKeys.nextElement());
            }
            
            Enumeration pKeys = parent.getKeys();
            while (pKeys.hasMoreElements()) {
                set.add(pKeys.nextElement());
            }
            
            return new Enumeration() {
                    private Iterator it = set.iterator();
                    public boolean hasMoreElements() { return it.hasNext(); }
                    public Object nextElement() { return it.next(); }
                };
        }
    }
    

    /**
     * Construct a new ProjectResourceBundle
     * 
     * @param projectName The name of the project to which the class belongs.
     *        It must be a proper prefix of the caller's package.
     * 
     * @param caller The calling class.
     *        This is used to get the package name to further construct
     *        the basename as well as to get the proper ClassLoader.
     * 
     * @param resourceName The name of the resource without the
     *        ".properties" extension
     * 
     * @throws MissingResourceException if projectName is not a prefix of
     *         the caller's package name, or if the resource could not be
     *         found/loaded.
     */
    public static ProjectResourceBundle getBundle(String projectName,
                                                  String packageName,
                                                  String resourceName)
        throws MissingResourceException
    {
        return getBundle(projectName, packageName, resourceName, null, null, null);
    }

    /**
     * Construct a new ProjectResourceBundle
     * 
     * @param projectName The name of the project to which the class belongs.
     *        It must be a proper prefix of the caller's package.
     * 
     * @param caller The calling class.
     *        This is used to get the package name to further construct
     *        the basename as well as to get the proper ClassLoader.
     * 
     * @param resourceName The name of the resource without the
     *        ".properties" extension
     * 
     * @throws MissingResourceException if projectName is not a prefix of
     *         the caller's package name, or if the resource could not be
     *         found/loaded.
     */
    public static ProjectResourceBundle getBundle(String projectName,
                                                  Class  caller,
                                                  String resourceName,
                                                  Locale locale)
        throws MissingResourceException
    {
        return getBundle(projectName,
                         caller,
                         resourceName,
                         locale,
                         null);
    }

    /**
     * Construct a new ProjectResourceBundle
     * 
     * @param projectName The name of the project to which the class belongs.
     *        It must be a proper prefix of the caller's package.
     * 
     * @param caller The calling class.
     *        This is used to get the package name to further construct
     *        the basename as well as to get the proper ClassLoader.
     * 
     * @param resourceName The name of the resource without the
     *        ".properties" extension
     * 
     * @param locale The locale
     * 
     * @throws MissingResourceException if projectName is not a prefix of
     *         the caller's package name, or if the resource could not be
     *         found/loaded.
     */
    public static ProjectResourceBundle getBundle(String projectName,
                                                  String packageName,
                                                  String resourceName,
                                                  Locale locale,
                                                  ClassLoader loader)
        throws MissingResourceException
    {
        return getBundle(projectName, packageName, resourceName, locale, loader, null);
    }

    /**
     * Construct a new ProjectResourceBundle
     * 
     * @param projectName The name of the project to which the class belongs.
     *        It must be a proper prefix of the caller's package.
     * 
     * @param caller The calling class.
     *        This is used to get the package name to further construct
     *        the basename as well as to get the proper ClassLoader.
     * 
     * @param resourceName The name of the resource without the
     *        ".properties" extension
     * 
     * @param locale The locale
     * 
     * @param extendsBundle If non-null, then this ExtendMessages will
     *         default to extendsBundle.
     * 
     * @throws MissingResourceException if projectName is not a prefix of
     *         the caller's package name, or if the resource could not be
     *         found/loaded.
     */
    public static ProjectResourceBundle getBundle(String projectName,
                                                  Class  caller,
                                                  String resourceName,
                                                  Locale locale,
                                                  ResourceBundle extendsBundle)
        throws MissingResourceException
    {
        return getBundle(projectName,
                         getPackage(caller.getClass().getName()),
                         resourceName,
                         locale,
                         caller.getClass().getClassLoader(),
                         extendsBundle);
    }

    /**
     * Construct a new ProjectResourceBundle
     * 
     * @param projectName The name of the project to which the class belongs.
     *        It must be a proper prefix of the caller's package.
     * 
     * @param caller The calling class.
     *        This is used to get the package name to further construct
     *        the basename as well as to get the proper ClassLoader.
     * 
     * @param resourceName The name of the resource without the
     *        ".properties" extension
     * 
     * @param locale The locale
     * 
     * @param extendsBundle If non-null, then this ExtendMessages will
     *         default to extendsBundle.
     * 
     * @throws MissingResourceException if projectName is not a prefix of
     *         the caller's package name, or if the resource could not be
     *         found/loaded.
     */
    public static ProjectResourceBundle getBundle(String projectName,
                                                  String packageName,
                                                  String resourceName,
                                                  Locale locale,
                                                  ClassLoader loader,
                                                  ResourceBundle extendsBundle)
        throws MissingResourceException
    {
        if (log.isDebugEnabled()) {
            log.debug("getBundle(" + projectName + ","
                                   + packageName + ","
                                   + resourceName + ","
                                   + String.valueOf(locale) + ",...)");
        }
        
        Context context = new Context();
        context.setLocale(locale);
        context.setLoader(loader);
        context.setProjectName(projectName);
        context.setResourceName(resourceName);
        context.setParentBundle(extendsBundle);

        packageName = context.validate(packageName);

        ProjectResourceBundle bundle = null;
        try {
            bundle = getBundle(context, packageName);
        } catch (RuntimeException e) {
            log.debug("Exception: ", e);
            throw e;
        }
        
        if (bundle == null) {
            throw new MissingResourceException("Cannot find resource '" +
                                               packageName + '.' + resourceName + "'",
                                               resourceName, "");
        }
        
        return bundle;
    }

    /**
     * get bundle...
     * - check cache
     * - try up hierarchy
     * - if at top of hierarchy, use (link to) context.getParentBundle()
     */
    private static synchronized ProjectResourceBundle getBundle(Context context, String packageName)
        throws MissingResourceException
    {
        String cacheKey = context.getCacheKey(packageName);
        
        ProjectResourceBundle prb = (ProjectResourceBundle)bundleCache.get(cacheKey);

        if (prb == null) {
            String name = packageName + '.' + context.getResourceName();
            ResourceBundle rb = context.loadBundle(packageName);
            ResourceBundle parent = context.getParentBundle(packageName);
            
            if (rb != null) {
                prb = new ProjectResourceBundle(name, rb);
                prb.setParent(parent);
                if (log.isDebugEnabled()) {
                    log.debug("Created " + prb + ", linked to parent " + String.valueOf(parent));
                }
            } else {
                if (parent != null) {
                    if (parent instanceof ProjectResourceBundle) {
                        prb = (ProjectResourceBundle)parent;
                    } else {
                        prb = new ProjectResourceBundle(name, parent);
                    }
                    if (log.isDebugEnabled()) {
                        log.debug("Root package not found, cross link to " + parent);
                    }
                }
            }

            if (prb != null) {
                // Cache the resource
                bundleCache.put(cacheKey, prb);
            }
        }

        return prb;
    }

    private static final String getPackage(String name) {
        return name.substring(0, name.lastIndexOf('.')).intern();
    }
    
    /**
      * Construct a new ProjectResourceBundle
      */
    private ProjectResourceBundle(String name, ResourceBundle bundle)
        throws MissingResourceException
    {
        this.resourceBundle = bundle;
        this.resourceName = name;
    }
    
    public String getResourceName() {
        return resourceName;
    }

    /**
     * Clears the internal cache
     */
    public static void clearCache()
    {
        bundleCache.clear();
    }
    
    public String toString() {
        return resourceName;
    }


    private static class Context {
        private Locale _locale;
        private ClassLoader _loader;
        private String _projectName;
        private String _resourceName;
        private ResourceBundle _parent;
        
        void setLocale(Locale l) {
            /* 1. Docs indicate that if locale is not specified,
             *    then the default local is used in it's place.
             * 2. A null value for locale is invalid.
             * 
             * Therefore, default...
             */
            _locale = (l == null) ? defaultLocale : l;
        }

        void setLoader(ClassLoader l) {
            _loader = (l != null) ? l : this.getClass().getClassLoader();
            // START FIX: http://nagoya.apache.org/bugzilla/show_bug.cgi?id=16868
            if (_loader == null) {
                _loader = ClassLoader.getSystemClassLoader();
            }
            // END FIX: http://nagoya.apache.org/bugzilla/show_bug.cgi?id=16868
        }
        
        void setProjectName(String name) { _projectName = name.intern(); }
        void setResourceName(String name) { _resourceName = name.intern(); }
        void setParentBundle(ResourceBundle b) { _parent = b; }
        
        Locale getLocale() { return _locale; }
        ClassLoader getLoader() { return _loader; }
        String getProjectName() { return _projectName; }
        String getResourceName() { return _resourceName; }
        ResourceBundle getParentBundle() { return _parent; }
    
        String getCacheKey(String packageName)
        {
            String loaderName = (_loader == null) ? "" : (":" + _loader.hashCode());
            return packageName + "." + _resourceName + ":" + _locale + ":" + defaultLocale + loaderName;
        }

        ResourceBundle loadBundle(String packageName)
        {
            try {
                return ResourceBundle.getBundle(packageName + '.' + _resourceName,
                                                _locale,
                                                _loader);
            } catch (MissingResourceException e) {
                // Deliberately surpressing print stack.. just the string for info.
                log.debug("loadBundle: Ignoring MissingResourceException: " + e.getMessage());
            }
            return null;
        }
    
        ResourceBundle getParentBundle(String packageName)
        {
            ResourceBundle p;
            if (packageName != _projectName) {
                p = getBundle(this, getPackage(packageName));
            } else {
                p = _parent;
                _parent = null;
            }
            return p;
        }
        
        String validate(String packageName)
            throws MissingResourceException
        {
            if (_projectName == null  ||  _projectName.length() == 0) {
                log.debug("Project name not specified");
                throw new MissingResourceException("Project name not specified",
                                                   "", "");
            }

            if (packageName == null  ||  packageName.length() == 0) {
                log.debug("Package name not specified");
                throw new MissingResourceException("Package not specified",
                                                   packageName, "");
            }
            packageName = packageName.intern();
    
            /* Ensure that project is a proper prefix of class.
             * Terminate project name with '.' to ensure proper match.
             */
            if (packageName != _projectName  &&  !packageName.startsWith(_projectName + '.')) {
                log.debug("Project not a prefix of Package");
                throw new MissingResourceException("Project '" + _projectName
                                 + "' must be a prefix of Package '"
                                 + packageName + "'",
                                 packageName + '.' + _resourceName, "");
            }
                
            return packageName;
        }
    }
}
