/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */

/* $Id: XMLResourceBundle.java 1610839 2014-07-15 20:25:58Z vhennebert $ */

package org.apache.fop.util;

import java.io.IOException;
import java.io.InputStream;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.Stack;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.stream.StreamSource;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import org.apache.xmlgraphics.util.QName;

/**
 * This class is a ResourceBundle that loads its contents from XML files instead of properties
 * files (like PropertiesResourceBundle).
 * <p>
 * The XML format for this resource bundle implementation is the following
 * (the same as Apache Cocoon's XMLResourceBundle):
 * <pre>
 * &lt;catalogue xml:lang="en"&gt;
 *   &lt;message key="key1"&gt;Message &lt;br/&gt; Value 1&lt;/message&gt;
 *   &lt;message key="key2"&gt;Message &lt;br/&gt; Value 1&lt;/message&gt;
 *   ...
 * &lt;/catalogue&gt;
 * </pre>
 */
public class XMLResourceBundle extends ResourceBundle {

    //Note: Some code here has been copied and adapted from Apache Harmony!

    private Properties resources = new Properties();

    private Locale locale;

    private static SAXTransformerFactory tFactory
        = (SAXTransformerFactory)SAXTransformerFactory.newInstance();

    /**
     * Creates a resource bundle from an InputStream.
     * @param in the stream to read from
     * @throws IOException if an I/O error occurs
     */
    public XMLResourceBundle(InputStream in) throws IOException {
        try {
            Transformer transformer = tFactory.newTransformer();
            StreamSource src = new StreamSource(in);
            SAXResult res = new SAXResult(new CatalogueHandler());
            transformer.transform(src, res);
        } catch (TransformerException e) {
            throw new IOException("Error while parsing XML resource bundle: " + e.getMessage());
        }
    }

    /**
     * Gets a resource bundle using the specified base name, default locale, and class loader.
     * @param baseName the base name of the resource bundle, a fully qualified class name
     * @param loader the class loader from which to load the resource bundle
     * @return a resource bundle for the given base name and the default locale
     * @throws MissingResourceException if no resource bundle for the specified base name can be
     *                                          found
     * @see java.util.ResourceBundle#getBundle(String)
     */
    public static ResourceBundle getXMLBundle(String baseName, ClassLoader loader)
                throws MissingResourceException {
        return getXMLBundle(baseName, Locale.getDefault(), loader);
    }

    /**
     * Gets a resource bundle using the specified base name, locale, and class loader.
     * @param baseName the base name of the resource bundle, a fully qualified class name
     * @param locale the locale for which a resource bundle is desired
     * @param loader the class loader from which to load the resource bundle
     * @return a resource bundle for the given base name and locale
     * @throws MissingResourceException if no resource bundle for the specified base name can be
     *                                          found
     * @see java.util.ResourceBundle#getBundle(String, Locale, ClassLoader)
     */
    public static ResourceBundle getXMLBundle(String baseName, Locale locale, ClassLoader loader)
                throws MissingResourceException {
        if (loader == null) {
            throw new NullPointerException("loader must not be null");
        }
        if (baseName == null) {
            throw new NullPointerException("baseName must not be null");
        }
        assert locale != null;
        ResourceBundle bundle;
        if (!locale.equals(Locale.getDefault())) {
            bundle = handleGetXMLBundle(baseName, "_" + locale, false, loader);
            if (bundle != null) {
                return bundle;
            }
        }
        bundle = handleGetXMLBundle(baseName, "_" + Locale.getDefault(), true, loader);
        if (bundle != null) {
            return bundle;
        }
        throw new MissingResourceException(
                baseName + " (" + locale + ")", baseName + '_' + locale, null);
    }

    static class MissingBundle extends ResourceBundle {
        public Enumeration getKeys() {
            return null;
        }

        public Object handleGetObject(String name) {
            return null;
        }
    }

    private static final ResourceBundle MISSING = new MissingBundle();
    private static final ResourceBundle MISSINGBASE = new MissingBundle();

    private static Map cache = new java.util.WeakHashMap();
    //<Object, Hashtable<String, ResourceBundle>>

    private static ResourceBundle handleGetXMLBundle(String base, String locale,
            boolean loadBase, final ClassLoader loader) {
        XMLResourceBundle bundle = null;
        String bundleName = base + locale;
        Object cacheKey = loader != null ? (Object) loader : (Object) "null";
        Hashtable loaderCache; //<String, ResourceBundle>
        synchronized (cache) {
            loaderCache = (Hashtable)cache.get(cacheKey);
            if (loaderCache == null) {
                loaderCache = new Hashtable();
                cache.put(cacheKey, loaderCache);
            }
        }
        ResourceBundle result = (ResourceBundle)loaderCache.get(bundleName);
        if (result != null) {
            if (result == MISSINGBASE) {
                return null;
            }
            if (result == MISSING) {
                if (!loadBase) {
                    return null;
                }
                String extension = strip(locale);
                if (extension == null) {
                    return null;
                }
                return handleGetXMLBundle(base, extension, loadBase, loader);
            }
            return result;
        }

        final String fileName = bundleName.replace('.', '/') + ".xml";
        InputStream stream = (InputStream)AccessController
                .doPrivileged(new PrivilegedAction() {
                    public Object run() {
                        return loader == null
                            ? ClassLoader.getSystemResourceAsStream(fileName)
                            : loader.getResourceAsStream(fileName);
                    }
                });
        if (stream != null) {
            try {
                try {
                    bundle = new XMLResourceBundle(stream);
                } finally {
                    stream.close();
                }
                bundle.setLocale(locale);
            } catch (IOException e) {
                throw new MissingResourceException(e.getMessage(), base, null);
            }
        }

        String extension = strip(locale);
        if (bundle != null) {
            if (extension != null) {
                ResourceBundle parent = handleGetXMLBundle(base, extension, true,
                        loader);
                if (parent != null) {
                    bundle.setParent(parent);
                }
            }
            loaderCache.put(bundleName, bundle);
            return bundle;
        }

        if (extension != null) {
            ResourceBundle fallback = handleGetXMLBundle(base, extension, loadBase, loader);
            if (fallback != null) {
                loaderCache.put(bundleName, fallback);
                return fallback;
            }
        }
        loaderCache.put(bundleName, loadBase ? MISSINGBASE : MISSING);
        return null;
    }

    private void setLocale(String name) {
        String language = "";
        String country = "";
        String variant = "";
        if (name.length() > 1) {
            int nextIndex = name.indexOf('_', 1);
            if (nextIndex == -1) {
                nextIndex = name.length();
            }
            language = name.substring(1, nextIndex);
            if (nextIndex + 1 < name.length()) {
                int index = nextIndex;
                nextIndex = name.indexOf('_', nextIndex + 1);
                if (nextIndex == -1) {
                    nextIndex = name.length();
                }
                country = name.substring(index + 1, nextIndex);
                if (nextIndex + 1 < name.length()) {
                    variant = name.substring(nextIndex + 1, name.length());
                }
            }
        }
        this.locale = new Locale(language, country, variant);
    }

    private static String strip(String name) {
        int index = name.lastIndexOf('_');
        if (index != -1) {
            return name.substring(0, index);
        }
        return null;
    }

    private Enumeration getLocalKeys() {
        return (Enumeration)resources.propertyNames();
    }

    /** {@inheritDoc} */
    public Locale getLocale() {
        return this.locale;
    }

    /** {@inheritDoc} */
    public Enumeration getKeys() {
        if (parent == null) {
            return getLocalKeys();
        }
        return new Enumeration() {
            private Enumeration local = getLocalKeys();
            private Enumeration pEnum = parent.getKeys();

            private Object nextElement;

            private boolean findNext() {
                if (nextElement != null) {
                    return true;
                }
                while (pEnum.hasMoreElements()) {
                    Object next = pEnum.nextElement();
                    if (!resources.containsKey(next)) {
                        nextElement = next;
                        return true;
                    }
                }
                return false;
            }

            public boolean hasMoreElements() {
                if (local.hasMoreElements()) {
                    return true;
                }
                return findNext();
            }

            public Object nextElement() {
                if (local.hasMoreElements()) {
                    return local.nextElement();
                }
                if (findNext()) {
                    Object result = nextElement;
                    nextElement = null;
                    return result;
                }
                // Cause an exception
                return pEnum.nextElement();
            }
        };
    }

    /** {@inheritDoc} */
    protected Object handleGetObject(String key) {
        if (key == null) {
            throw new NullPointerException("key must not be null");
        }
        return resources.get(key);
    }

    /** {@inheritDoc} */
    public String toString() {
        return "XMLResourceBundle: " + getLocale();
    }

    private class CatalogueHandler extends DefaultHandler {

        private static final String CATALOGUE = "catalogue";
        private static final String MESSAGE = "message";

        private StringBuffer valueBuffer = new StringBuffer();
        private Stack elementStack = new Stack();
        private String currentKey;

        private boolean isOwnNamespace(String uri) {
            return ("".equals(uri));
        }

        private QName getParentElementName() {
            return (QName)elementStack.peek();
        }

        /** {@inheritDoc} */
        public void startElement(String uri, String localName, String qName,
                Attributes atts) throws SAXException {
            super.startElement(uri, localName, qName, atts);
            QName elementName = new QName(uri, qName);
            if (isOwnNamespace(uri)) {
                if (CATALOGUE.equals(localName)) {
                    //nop
                } else if (MESSAGE.equals(localName)) {
                    if (!CATALOGUE.equals(getParentElementName().getLocalName())) {
                        throw new SAXException(MESSAGE + " must be a child of " + CATALOGUE);
                    }
                    this.currentKey = atts.getValue("key");
                } else {
                    throw new SAXException("Invalid element name: " + elementName);
                }
            } else {
                //ignore
            }
            this.valueBuffer.setLength(0);
            elementStack.push(elementName);
        }

        /** {@inheritDoc} */
        public void endElement(String uri, String localName, String qName) throws SAXException {
            super.endElement(uri, localName, qName);
            elementStack.pop();
            if (isOwnNamespace(uri)) {
                if (CATALOGUE.equals(localName)) {
                    //nop
                } else if (MESSAGE.equals(localName)) {
                    if (this.currentKey == null) {
                        throw new SAXException(
                                "current key is null (attribute 'key' might be mistyped)");
                    }
                    resources.put(this.currentKey, this.valueBuffer.toString());
                    this.currentKey = null;
                }
            } else {
                //ignore
            }
            this.valueBuffer.setLength(0);
        }

        /** {@inheritDoc} */
        public void characters(char[] ch, int start, int length) throws SAXException {
            super.characters(ch, start, length);
            valueBuffer.append(ch, start, length);
        }

    }

}
