/*
 * $Header: /home/cvspublic/jakarta-slide/proposals/tamino/src/urm/org/apache/slide/urm/utils/resourceutilities/message/XMLMessageResourceBundle.java,v 1.4 2005/03/02 10:53:36 eckehard Exp $
 * $Revision: 1.4 $
 * $Date: 2005/03/02 10:53:36 $
 *
 * ====================================================================
 *
 * Copyright 1999-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.slide.urm.utils.resourceutilities.message;

// import list

import org.apache.slide.urm.utils.resourceutilities.AbstractXMLBasedResourceBundle;
import org.apache.slide.urm.utils.resourceutilities.XMLResourceBundleFailureHandler;

import java.io.IOException;

import java.net.URL;

import java.util.Locale;
import java.util.Stack;
import java.util.ResourceBundle;
import java.util.MissingResourceException;

import javax.xml.parsers.ParserConfigurationException;

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

import org.xml.sax.helpers.DefaultHandler;

/**
 ** This is an extension of java.util.ResourceBundle that parses a (Tamino) XML message
 ** resource file and provides the contents as entries accepted by the Frameworks' MessageService.
 ** <br/>
 ** Due to implementation weaknesses of the java.util.ResourceBundle class you must
 ** use {@link #getBundle(String baseName, Locale locale, ClassLoader loader)
 ** getBundle(String baseName, Locale locale, ClassLoader loader)} and not one of the shorter
 ** alternatives with less parameters. But the parameters locale and loader may be <code>
 ** null</code>.<br/><br/>
 **
 ** Example: The XML document...<br/><br/>
 **
 ** <pre>
 ** &lt;?xml version="1.0" encoding="utf-8"?&gt;
 ** &lt;messages-chapter&gt;
 **     &lt;title&gt;Sample Message&lt;/title&gt;
 **     &lt;message-info id="INOXDW0001" switch="show" developer="rsk"&gt;
 **         &lt;message&gt;
 **             &lt;gui-title&gt;Oops...&lt;/gui-title&gt;
 **             &lt;mess-id&gt;INOXDW0001&lt;/mess-id&gt;
 **             &lt;mess-text&gt;Something very, very bad happened.&lt;/mess-text&gt;
 **         &lt;/message&gt;
 **         &lt;mess-expl&gt;
 **             &lt;gui-title&gt;Explanation&lt;/gui-title&gt;
 **             &lt;p&gt;So you got a problem!&lt;/p&gt;
 **         &lt;/mess-expl&gt;
 **         &lt;action&gt;
 **             &lt;gui-title&gt;Action&lt;/gui-title&gt;
 **             &lt;p&gt;Twist and shout!&lt;/p&gt;
 **         &lt;/action&gt;
 **     &lt;/message-info&gt;
 ** &lt;/messages-chapter&gt;
 ** </pre>
 **
 ** ...will result in a ResourceBundle containing the following entries:<br/><br/>
 **
 ** <pre>
 ** INOXDW0001: INOXDW0001 Something very, very bad happened.
 ** INOXDW0001_TITLE: Oops...
 ** INOXDW0001_EXPLANATION_TITLE: Explanation
 ** INOXDW0001_EXPLANATION: So you got a problem!
 ** INOXDW0001_ACTION_TITLE: Action
 ** INOXDW0001_ACTION: Twist and shout!
 ** INOXDW0001_HELP_ID: INOXDW0001
 ** </pre>
 **
 ** <b>If you are already familiar with former versions of this class you may have noticed
 ** that the <code>&lt;title&gt;</code> tags have been replaced by <code>&lt;gui-title&gt;</code>.
 ** This is because the current Tamino message DTD does not support the <code>&lt;title&gt;</code>
 ** tags but only <code>&lt;gui-title&gt;</code> tags. So if you want your document to be valid you
 ** better use these new tags. Nevertheless XML files containing the 'old' <code>&lt;title&gt;</code>
 ** tags will still be parsed correctly.</b><br/><br/>
 **
 ** This class is intended to be used in conjunction with the Frameworks' MessageService, but
 ** you can use it like a regular ResourceBundle. You have the same internationalization
 ** support as in the original ResourceBundle class, means if you  call:<br/><br/>
 **
 ** <code>XMLMessageResourceBundle.getBundle("message", Locale.GERMANY, null)</code><br/><br/>
 **
 ** assuming the default locale is "en_US", the classpath will be searched for the
 ** following files:<br/><br/>
 **
 ** <pre>
 ** message_de_DE.xml
 ** message_de.xml
 ** message_en_US.xml
 ** message_en.xml
 ** message.xml
 ** </pre>
 **
 ** <b><p/>
 ** Now a set of (ANT-) scripts is available that ease the handling of the XML based message files
 ** in conjunction with XMLMessageResource and {@link ResourceMessage ResourceMessage}.
 ** See <a href="../../../../../../internal/AboutMessageConstants.html">AboutMessageConstants.html</a> for more information.
 ** <p/></b>
 **
 ** @version $Revision: 1.4 $
 **
 ** @author rsk@softwareag.com
 **/
public class XMLMessageResourceBundle extends AbstractXMLBasedResourceBundle implements MessageConstants  {
    
    /**
     ** The XMLBasedResourceBundleFactory used to provide AbstractXMLBasedResourceBundle
     ** a possibility to create instances of concrete subclasses.
     **
     ** @version $Revision: 1.4 $
     **
     ** @author rsk@softwareag.com
     **/
    protected static class XMLMessageResourceBundleFactory implements XMLBasedResourceBundleFactory {
        
        /**
         ** Creates a (subclass of) AbstractXMLBasedResourceBundle.
         **
         ** @param      resourceURL  the URL of the resource.
         ** @param      locale       the Locale associated with this ResourceBundle.
         **
         ** @return     an instance of the concrete AbstractXMLBasedResourceBundle
         **             subclass.
         **
         ** @exception  ParserConfigurationException if parsing the XML document failed.
         ** @exception  SAXException if parsing the XML document failed.
         ** @exception  IOException if parsing the XML document failed.
         **/
        public AbstractXMLBasedResourceBundle createXMLResourceBundle(URL resourceURL, Locale locale) throws SAXException, ParserConfigurationException, IOException {
            return new XMLMessageResourceBundle(resourceURL, locale);
        }
    }
    
    /**
     ** The XMLBasedResourceBundleFactory used to provide AbstractXMLBasedResourceBundle
     ** a possibility to create instances of concrete subclasses.
     **/
    protected static XMLMessageResourceBundleFactory factory = new XMLMessageResourceBundleFactory();
    
    
    
    
    
    /**
     ** Creates a XMLMessageResourceBundle.
     **
     ** @param resourceURL the URL of the resource.
     ** @param locale the Locale associated with this ResourceBundle.
     **
     ** @exception  ParserConfigurationException if parsing the XML document failed.
     ** @exception  SAXException if parsing the XML document failed.
     ** @exception  IOException if parsing the XML document failed.
     **/
    protected XMLMessageResourceBundle(URL resourceURL, Locale locale) throws SAXException, ParserConfigurationException, IOException {
        super(resourceURL, locale);
    }
    
    /**
     ** Retrieves the appropriate ResourceBundle.
     **
     ** @param baseName the base name of the resource (without extension).
     ** @param locale the desrired Locale. May be <code>null</code>.
     ** @param loader the ClassLoader to load the resource from. May be <code>null</code>.
     **
     ** @return the ResourceBundle if one was found.
     **
     **/
    public static ResourceBundle getBundle(String baseName, Locale locale, ClassLoader loader) {
        return getBundle(baseName, locale, loader, null);
    }
    
    /**
     ** Retrieves the appropriate ResourceBundle.
     ** An <code>XMLResourceBundleFailureHandler</code> may be passed in order
     ** to react on parsing failures.
     **
     ** @param     baseName          the base name of the resource (without extension).
     ** @param     locale            the desrired Locale. May be <code>null</code>.
     ** @param     loader            the ClassLoader to load the resource from. May be <code>null</code>.
     ** @param     failureHandler    the XMLResourceBundleFailureHandler used to react on failures.
     **                              If <code>null</code>, any occurring failures will be ignored.
     **
     ** @return    the ResourceBundle if one was found.
     **
     ** @exception MissingResourceException  may be thrown by a XMLResourceBundleFailureHandler
     **                                      to indicate that parsing the XML file failed.
     **/
    public static ResourceBundle getBundle(String baseName, Locale locale, ClassLoader loader, XMLResourceBundleFailureHandler failureHandler) throws MissingResourceException {
        return getBundle(baseName, locale, loader, failureHandler, factory);
    }
    
    
    /**
     ** Returns the DefaultHandler used to parse the XML document. It is the
     ** DefaultHandler's responsibility to fill the ResourceBundle with properties
     ** based on the content of XML document.
     **
     ** @pre        true
     ** @post       true
     **
     ** @return     the DefaultHandler used to parse the XML document.
     **/
    protected DefaultHandler getDefaultHandler() {
        return new SAXHandler(this);
    }
    
    
    
    /**
     ** This class handles the SAX events and generates appropriate entries
     ** in the given ResourceBundle.
     **
     ** @version $Revision: 1.4 $
     **
     ** @author rsk@softwareag.com
     **/
    protected static class SAXHandler extends DefaultHandler {
        
        /**
         ** String constant for tag name "message-info".
         **/
        public final static String MESSAGE_INFO_TAG = "message-info";
        
        /**
         ** String constant for tag name "message".
         **/
        public final static String MESSAGE_TAG = "message";
        
        /**
         ** String constant for tag name "mess-text".
         **/
        public final static String MESSAGE_TEXT_TAG = "mess-text";
        
        /**
         ** String constant for tag name "mess-expl".
         **/
        public final static String MESS_EXPL_TAG = "mess-expl";
        
        /**
         ** String constant for tag name "p".
         **/
        public final static String P_TAG = "p";
        
        /**
         ** String constant for tag name "title".
         **/
        public final static String TITLE_TAG = "title";
        
        /**
         ** String constant for tag name "gui-title".
         **/
        public final static String GUI_TITLE_TAG = "gui-title";
        
        /**
         ** String constant for tag name "action".
         **/
        public final static String ACTION_TAG = "action";
        
        
        /**
         ** String constant for attribute name "id".
         **/
        public final static String ID_ATTRIBUTE = "id";
        
        /**
         ** String constant for an empty ("") string.
         **/
        public final static String EMPTY_STRING = "";
        
        
        
        
        /**
         ** The XMLMessageResourceBundle this Handler "is working for".
         **/
        protected XMLMessageResourceBundle resourceBundle = null;
        
        /**
         ** The key of the current property.
         **/
        protected String propertyKey = EMPTY_STRING;
        
        /**
         ** The stack of property keys.
         **/
        protected Stack propertyKeyStack = new Stack();
        
        /**
         ** The value of the current property.
         **/
        protected StringBuffer propertyValue = new StringBuffer();
        
        /**
         ** The stack of property values.
         **/
        protected Stack propertyValueStack = new Stack();
        
        /**
         ** The stack of element names.
         **/
        protected Stack elementNameStack = new Stack();
        
        /**
         ** The name of the current element.
         **/
        protected String elementName = EMPTY_STRING;
        
        /**
         ** The id of the message.
         **/
        protected String id = null;
        
        /**
         ** Indicates if a message is currently being parsed.
         **/
        protected boolean isMessageInfo = false;
        
        
        /**
         ** Creates a SAXHandler.
         **
         ** @param resourceBundle the XMLMessageResourceBundle this Handler "is working for".
         **/
        public SAXHandler(XMLMessageResourceBundle resourceBundle) {
            
            this.resourceBundle = resourceBundle;
        }
        
        /**
         ** Returns the property key for the given element if one exists,
         ** otherwise <code>EMPTY_STRING</code> is returned.
         **
         ** @param  elementName  the name of the element for which to return
         **                      the property key.
         **
         ** @return the property key for the given element if one exists,
         **         otherwise <code>EMPTY_STRING</code> is returned.
         **/
        protected String getPropertyKeyForElement(String elementName) {
            
            String key = EMPTY_STRING;
            
            if (id != null) {
                
                if (elementName.equals(MESSAGE_TEXT_TAG)) {
                    key = id;
                }
                else if ( elementName.equals(TITLE_TAG) || elementName.equals(GUI_TITLE_TAG) ) {
                    
                    String parentElement = (String)elementNameStack.peek();
                    
                    if (MESSAGE_TAG.equals(parentElement)) {
                        
                        key = id + TITLE_SUFFIX;
                    }
                    else if (MESS_EXPL_TAG.equals(parentElement)) {
                        
                        key = id + EXPLANATION_TITLE_SUFFIX;
                    }
                    else if (ACTION_TAG.equals(parentElement)) {
                        
                        key = id + ACTION_TITLE_SUFFIX;
                    }
                }
                else if (elementName.equals(P_TAG)) {
                    
                    String parentElement = (String)elementNameStack.peek();
                    if (MESS_EXPL_TAG.equals(parentElement)) {
                        
                        key = id + EXPLANATION_SUFFIX;
                    }
                    else if (ACTION_TAG.equals(parentElement)) {
                        
                        key = id + ACTION_SUFFIX;
                    }
                }
            }
            return key;
        }
        
        /**
         ** Processes Attributes, currently only the id attribute of the message-info
         ** tag is processed.
         **
         ** @param elementName the name of the element to which the attributes belong.
         ** @param attributes the attributes.
         **/
        protected void processAttributes(String elementName, Attributes attributes) {
            
            if (elementName.equals(MESSAGE_INFO_TAG)) {
                id = attributes.getValue(ID_ATTRIBUTE);
                
                // set help id
                if (id != null) {
                    resourceBundle.put(id + HELP_ID_SUFFIX, id);
                }
            }
        }
        
        /**
         ** Called when the start tag of an element has been detected.
         **
         ** @param  namespaceURI   the namespace URI of the element (if existing).
         ** @param  localName      the local name, e.g. the name without namespace prefix.
         **                        If no namespace prefix is set, <code>null</code>
         **                        is returned.
         ** @param  qualifiedName  the (full qualified) name of the element.
         ** @param  attributes     the attributes of the element.
         **
         ** @throws SAXException   if parsing fails for any reason.
         **/
        public void startElement(String namespaceURI, String localName, String qualifiedName, Attributes attributes) throws SAXException {
            
            if (qualifiedName.equals(MESSAGE_INFO_TAG)) {
                isMessageInfo = true;
            }
            
            if (isMessageInfo) {
                
                // push old values on stack
                propertyKeyStack.push(propertyKey);
                propertyValueStack.push(propertyValue);
                elementNameStack.push(elementName);
                
                // set up new values
                propertyKey = EMPTY_STRING;
                propertyValue = new StringBuffer();
                elementName = qualifiedName;
                
                processAttributes(qualifiedName, attributes);
                propertyKey = getPropertyKeyForElement(qualifiedName);
                
                // message itself starts with the error code
                if (propertyKey.equals(id))  {
                    propertyValue.append(id);
                    propertyValue.append(" ");
                }
            }
        }
        
        /**
         ** Called when the end tag of an element has been detected.
         **
         ** @param  namespaceURI   the namespace URI of the element (if existing).
         ** @param  localName      the local name, e.g. the name without namespace prefix.
         **                        If no namespace prefix is set, <code>null</code>
         **                        is returned.
         ** @param  qualifiedName  the (full qualified) name of the element.
         **
         ** @throws SAXException   if parsing fails for any reason.
         **/
        public void endElement(String namespaceURI, String localName, String qualifiedName) throws SAXException {
            
            if (isMessageInfo) {
                
                if (propertyKey != EMPTY_STRING) {
                    resourceBundle.put(propertyKey, propertyValue.toString());
                }
                
                // pop old values from stack
                propertyKey = (String)propertyKeyStack.pop();
                propertyValue = (StringBuffer)propertyValueStack.pop();
                elementName = (String)elementNameStack.pop();
            }
            
            if (qualifiedName.equals(MESSAGE_INFO_TAG)) {
                isMessageInfo = false;
            }
        }
        
        /**
         ** Called when characters (text) has been detected.
         **
         ** @param  ch      the char array containing the text.
         ** @param  start   the start offset of the char array.
         ** @param  length  the length of the char array.
         **
         ** @throws SAXException   if parsing fails for any reason.
         **/
        public void characters(char[] ch, int start, int length) throws SAXException {
            
            if (isMessageInfo) {
                
                if (propertyKey != EMPTY_STRING) {
                    propertyValue.append(ch, start, length);
                }
            }
        }
        
        
        /**
         ** Called when a SAXException of severity <code>warning</code> occurred.
         **
         ** @param  e  the SAXException that occurred.
         **
         ** @throws  SAXException  that was passed as parameter.
         **/
        public void warning(SAXException e) throws SAXException {
            throw e;
        }
        
        /**
         ** Called when a SAXException of severity <code>error</code> occurred.
         **
         ** @param  e  the SAXException that occurred.
         **
         ** @throws  SAXException  that was passed as parameter.
         **/
        public void error(SAXException e) throws SAXException {
            throw e;
        }
        
        /**
         ** Called when a SAXException of severity <code>fatal error</code> occurred.
         **
         ** @param  e  the SAXException that occurred.
         **
         ** @throws  SAXException  that was passed as parameter.
         **/
        public void fatalError(SAXException e) throws SAXException {
            throw e;
        }
        
    }
    
    
}
