/* This file is part of NanoXML.
 *
 * $Revision: 1.7 $
 * $Date: 2000/05/23 19:01:24 $
 * $Name:  $
 *
 * Copyright (C) 2000 Marc De Scheemaecker, All Rights Reserved.
 *
 * This software is provided 'as-is', without any express or implied warranty.
 * In no event will the authors be held liable for any damages arising from the
 * use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 *  1. The origin of this software must not be misrepresented; you must not
 *     claim that you wrote the original software. If you use this software in
 *     a product, an acknowledgment in the product documentation would be
 *     appreciated but is not required.
 *
 *  2. Altered source versions must be plainly marked as such, and must not be
 *     misrepresented as being the original software.
 *
 *  3. This notice may not be removed or altered from any source distribution.
 */

// renamed the package from nanoxml to net.sourceforge.nanoxml <conan@warp.cx>
package net.sourceforge.nanoxml;


import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;


/**
 * $Revision: 1.7 $<BR>
 * $Date: 2000/05/23 19:01:24 $<BR>
 * $Name:  $<P>
 *
 * XMLElement is a representation of an XML object. The object is able to parse
 * XML code.
 *
 * @author Marc De Scheemaecker
 *         &lt;<A HREF="mailto:Marc.DeScheemaecker@advalvas.be"
 *         >Marc.DeScheemaecker@advalvas.be</A>&gt;
 */
public class XMLElement
{

    /**
     * Major version of NanoXML.
     */
    public static final int NANOXML_MAJOR_VERSION = 1;


    /**
     * Minor version of NanoXML.
     */
    public static final int NANOXML_MINOR_VERSION = 3;

    
    /**
     * The attributes given to the object.
     */
    protected Properties attributes;
    
    
    /**
     * Subobjects of the object. The subobjects are of class XMLElement
     * themselves.
     */
    protected Vector children;
    
    
    /**
     * The class of the object (the name indicated in the tag).
     */
    protected String tagName;
    

    /**
     * The #PCDATA content of the object. If there is no such content, this
     * field is null.
     */
    protected String contents;
    
        
    /**
     * Conversion table for &...; tags.
     */
    protected Properties conversionTable;


    /**
     * Whether to skip leading whitespace in CDATA.
     */
    protected boolean skipLeadingWhitespace;

    
    /**
     * Creates a new XML element.
     */
    public XMLElement()
    {
        this(new Properties(), false, true);
    }
    

    /**
     * Creates a new XML element.
     */
    public XMLElement(Properties conversionTable)
    {
        this(conversionTable, false, true);
    }
    

    /**
     * Creates a new XML element.
     */
    public XMLElement(boolean skipLeadingWhitespace)
    {
        this(new Properties(), skipLeadingWhitespace, true);
    }
    

    /**
     * Creates a new XML element.
     */
    public XMLElement(Properties conversionTable,
                      boolean    skipLeadingWhitespace)
    {
        this(conversionTable, skipLeadingWhitespace, true);
    }
    

    /**
     * Creates a new XML element.
     */
    public XMLElement(Properties conversionTable,
                      boolean    skipLeadingWhitespace,
                      boolean    fillBasicConversionTable)
    {
        this.skipLeadingWhitespace = skipLeadingWhitespace;
        this.tagName = null;
        this.contents = null;
        this.attributes = new Properties();
        this.children = new Vector();
        this.conversionTable = conversionTable;

        if (fillBasicConversionTable) {
            this.conversionTable.put("lt", "<");
            this.conversionTable.put("gt", ">");
            this.conversionTable.put("quot", "\"");
            this.conversionTable.put("apos", "'");
            this.conversionTable.put("amp", "&");
        }
    }
    
    
    /**
     * Returns the number of subobjects of the object.
     */
    public int countChildren()
    {
        return this.children.size();
    }
    
    
    /**
     * Enumerates the subobjects of the object.
     */
    public Enumeration enumerateChildren()
    {
        return this.children.elements();
    }
    
    
    /**
     * Returns the subobjects of the object.
     */
    public Vector getChildren()
    {
        return this.children;
    }
    
    
    /**
     * Returns the #PCDATA content of the object. If there is no such content,
     * null is returned.
     */
    public String getContents()
    {
        return this.contents;
    }
    
    
    /**
     * Returns a property by looking up a key in a hashtable. 
     * If the property doesn't exist, the value corresponding to defaultValue
     * is returned.
     */
    public int getIntProperty(String    key,
                              Hashtable valueSet,
                              String    defaultValue)
    {
        String val = this.attributes.getProperty(key);
        Integer result;
        
        if (val == null) {
            val = defaultValue;
        }
        
        try {
            result = (Integer)(valueSet.get(val));
        }
        catch (ClassCastException e) {
            String msg = "Invalid valueset given to method getProperty()";
            throw new XMLParseException(this.getTagName(), msg);
        }
        
        if (result == null) {
            String msg
                = "Attribute " + key + " does not contain a valid value";
            throw new XMLParseException(this.getTagName(), msg);
        }
        
        return result.intValue();
    }
    

    /**
     * Returns a property of the object.
     */
    public String getProperty(String key)
    {
        return this.attributes.getProperty(key);
    }
    
    
    /**
     * Returns a property of the object. 
     * If the property doesn't exist, defaultValue is returned.
     */
    public String getProperty(String key,
                              String defaultValue)
    {
        return this.attributes.getProperty(key, defaultValue);
    }
    
    
    /**
     * Returns an integer property of the object. 
     * If the property doesn't exist, defaultValue is returned.
     */
    public int getProperty(String key,
                           int    defaultValue)
    {
        String val = this.attributes.getProperty(key);
        
        if (val == null) {
            return defaultValue;
        }
        else {
            try {
                return Integer.parseInt(val);
            }
            catch (NumberFormatException e) {
                String msg = "Attribute " + key
                    + " does not contain an integer value";
                throw new XMLParseException(this.getTagName(), msg);
            }
        }
    }
    
    
    /**
     * Returns a floating point property of the object.
     * If the property doesn't exist, defaultValue is returned.
     */
    public double getProperty(String key,
                              double defaultValue)
    {
        String val = this.attributes.getProperty(key);
        
        if (val == null) {
            return defaultValue;
        }
        else {
            try {
                return Double.valueOf(val).doubleValue();
            }
            catch (NumberFormatException e) {
                String msg = "Attribute " + key
                    + " does not contain a numeric value";
                throw new XMLParseException(this.getTagName(), msg);
            }
        }
    }
    
    
    /**
     * Returns a boolean property of the object.
     */
    public boolean getProperty(String  key,
                               String  trueValue,
                               String  falseValue,
                               boolean defaultValue)
    {
        String val = this.attributes.getProperty(key);
        
        if (val == null) {
            return defaultValue;
        }
        else if (val.equals(trueValue)) {
            return true;
        }
        else if (val.equals(falseValue)) {
            return false;
        }
        else {
            String msg = "Attribute " + key + " may only contain the values \""
                + trueValue + "\" or \"" + falseValue + "\"";
            throw new XMLParseException(this.getTagName(), msg);
        }
    }
    
    
    /**
     * Returns a property by looking up a key in a hashtable.
     * If the property doesn't exist, the value corresponding to defaultValue
     * is returned.
     */
    public Object getProperty(String    key,
                              Hashtable valueSet,
                              String    defaultValue)
    {
        String val = this.attributes.getProperty(key);
        
        if (val == null) {
            val = defaultValue;
        }
        
        Object result = valueSet.get(val);
        
        if (result == null) {
            String msg
                = "Attribute " + key + " does not contain a valid value";
            throw new XMLParseException(this.getTagName(), msg);
        }
        
        return result;
    }
    
                                        
    /**
     * Returns a property by looking up a key in a hashtable.
     * If the property doesn't exist, the value corresponding to defaultValue
     * is returned.
     */
    public String getStringProperty(String    key,
                                    Hashtable valueSet,
                                    String    defaultValue)
    {
        String val = this.attributes.getProperty(key);
        String result;
        
        if (val == null) {
            val = defaultValue;
        }
        
        try {
            result = (String)(valueSet.get(val));
        }
        catch (ClassCastException e) {
            String msg
                = "Invalid valueset given to method getStringProperty()";
            throw new XMLParseException(this.getTagName(), msg);
        }
        
        if (result == null) {
            String msg
                = "Attribute " + key + " does not contain a valid value";
            throw new XMLParseException(this.getTagName(), msg);
        }
        
        return result;
    }
    
                                        
    /**
     * Returns a property by looking up a key in a hashtable. 
     * If the value is not defined in the hashtable, the value is considered to
     * be an integer.
     * If the property doesn't exist, the value corresponding to defaultValue
     * is returned.
     */
    public int getSpecialIntProperty(String    key,
                                     Hashtable valueSet,
                                     String    defaultValue)
    {
        String val = this.attributes.getProperty(key);
        Integer result;
        
        if (val == null) {
            val = defaultValue;
        }
        
        try {
            result = (Integer)(valueSet.get(val));
        }
        catch (ClassCastException e) {
            String msg = "Invalid valueset given to method getProperty()";
            throw new XMLParseException(this.getTagName(), msg);
        }
        
        if (result == null) {
            try {
                return Integer.parseInt(val);
            }
            catch (NumberFormatException e) {
                String msg = "Attribute " + key
                    + " does not contain a valid value";
                throw new XMLParseException(this.getTagName(), msg);
            }
        }
        
        return result.intValue();
    }
    
                                        
    /**
     * Returns a property by looking up a key in a hashtable.
     * If the value is not defined in the hashtable, the value is considered to
     * be a floating point.
     * If the property doesn't exist, the value corresponding to defaultValue
     * is returned.
     */
    public double getSpecialDoubleProperty(String    key,
                                           Hashtable valueSet,
                                           String    defaultValue)
    {
        String val = this.attributes.getProperty(key);
        Double result;
        
        if (val == null) {
            val = defaultValue;
        }
        
        try {
            result = (Double)(valueSet.get(val));
        }
        catch (ClassCastException e) {
            String msg = "Invalid valueset given to method getProperty()";
            throw new XMLParseException(this.getTagName(), msg);
        }
        
        if (result == null) {
            try {
                result = Double.valueOf(val);
            }
            catch (NumberFormatException e) {
                String msg = "Attribute " + key
                    + " does not contain a valid value";
                throw new XMLParseException(this.getTagName(), msg);
            }
        }
        
        return result.doubleValue();
    }
    
                                        
    /**
     * Returns the class (i.e. the name indicated in the tag) of the object.
     * 
     */
    public String getTagName()
    {
        return this.tagName;
    }


    /**
     * Checks whether a character may be part of an identifier.
     */
    protected boolean isIdentifierChar(char ch)
    {
        return (((ch >= 'A') && (ch <= 'Z')) || ((ch >= 'a') && (ch <= 'z'))
                || ((ch >= '0') && (ch <= '9')) || (".-_:".indexOf(ch) >= 0));
    }
    
    
    /**
     * Parses an XML definition.
     */
    public int parseString(String string,
                           int    offset)
    {
        offset = this.skipWhiteSpace(string, offset);
        
        if (offset < 0) {
            return -1;
        }
        
        offset = this.skipPreamble(string, offset);
        
        if (offset < 0) {
            return -1;
        }
        
        offset = this.scanTagName(string, offset);
        offset = this.scanAttributes(string, offset);
        offset = this.scanContent(string, offset);
        
        if (this.contents != null) {
            this.scanChildren();
            
            if (this.children.size() > 0) {
                this.contents = null;
            }
            else {
                this.processContents();
            }
        }
        
        return offset;
    }

    
    /**
     * Removes extraneous whitespaces after newlines and convert those newlines
     * into spaces.
     */
    protected void processContents()
    {
        if (! this.skipLeadingWhitespace) {
            this.contents = this.decodeString(this.contents);
            return;
        }
        
        StringBuffer result = new StringBuffer(this.contents.length());
        StringTokenizer tok = new StringTokenizer(this.contents, "\r\n");
        
        while (tok.hasMoreTokens()) {
            String str = tok.nextToken();
            int offset = this.skipWhiteSpace(str, 0);
            
            if (offset >= 0) {
                result.append(str.substring(offset));
            }
        }
        
        this.contents = this.decodeString(result.toString());
    }
    
    
    /**
     * Scans the attributes of the object.
     */
    private int scanAttributes(String string,
                               int    offset)
    {
        String key, value;
        
        for (;;) {
            offset = this.skipWhiteSpace(string, offset);
            
            if (offset < 0) {
                throw new XMLParseException(this.tagName, "Tag not delimited");
            }
            
            char ch = string.charAt(offset);
        
            if ((ch == '/') || (ch == '>')) {
                break;
            }
            
            offset = this.scanOneAttribute(string, offset);
        }
        
        return offset;
    }
    
    
    /**
     * Searches the content for child objects. If such objects exist, the
     * content is reduced to null.
     */
    protected void scanChildren()
    {
        int offset = this.skipWhiteSpace(this.contents, 0);
        
        while (offset >= 0) {
            XMLElement child = new XMLElement(this.conversionTable,
                                              this.skipLeadingWhitespace,
                                              false);
            offset = child.parseString(this.contents, offset);
            
            if (offset > 0) {
                this.children.addElement(child);
            }
            
            offset = this.skipWhiteSpace(this.contents, offset);
        }
    }
    
    
    /**
     * Scans the content of the object.
     */
    protected int scanContent(String string,
                            int    offset)
    {
        if (offset < 0) {
            throw new XMLParseException(this.tagName, "Premature end of file");
        }
 
        if (string.charAt(offset) == '/') {
            this.contents = null;
            
            if (string.charAt(offset + 1) != '>') {
                String msg = "Tag is not closed correctly";
                throw new XMLParseException(this.tagName, msg);
            }
            
            return offset + 2;
        }
        
        if (string.charAt(offset) != '>') {
            String msg = "Tag is not closed correctly";
            throw new XMLParseException(this.tagName, msg);
        }

        if (this.skipLeadingWhitespace) {
            offset = this.skipWhiteSpace(string, offset + 1);
        }
        else {
            offset++;
        }
        
        int begin = offset;
        int level = 0;

        while ((offset > 0)
               && (offset < string.length() - 2 - this.tagName.length())) {
            String tagToCheck
                = string.substring(offset,
                                   this.tagName.length() + 2 + offset);

            if (tagToCheck.substring(0, tagToCheck.length() - 1)
                    .equalsIgnoreCase("<" + this.tagName)) {
                int off2 = offset + this.tagName.length() + 1;
                int off3 = this.skipWhiteSpace(string, off2);
                
                if (off3 < 0) {
                    throw new XMLParseException(this.tagName,
                                                "No closing tag");
                }

                if ((string.charAt(off3) == '>')
                        || ((off2 != off3) && (string.charAt(off3) != '/'))) {
                    level++;
                }
            }
            else if (tagToCheck.equalsIgnoreCase("</" + this.tagName)) {
                int off2 = offset + this.tagName.length() + 2;
                off2 = this.skipWhiteSpace(string, off2);
                
                if (off2 < 0) {
                    throw new XMLParseException(this.tagName,
                                                "No closing tag");
                }
                
                if (string.charAt(off2) == '>') {
                    level--;

                    if (level >= 0) {
                        offset++;
                        continue;
                    }

                    while ((offset > 0)
                           && (" \t\r\n"
                               .indexOf(string.charAt(offset - 1)) >= 0)) {
                        offset--;
                    }

                    if (offset <= 0) {
                        break;
                    }

                    if (begin >= offset) {
                        this.contents = "";
                    }
                    else {
                        this.contents = string.substring(begin, offset);
                    }
                    
                    return off2 + 1;
                }
            }
            
            offset++;
        }

        String msg = "Tag is not closed correctly";
        throw new XMLParseException(this.tagName, msg);
    }
    
    
    /**
     * Scans an identifier.
     */
    protected String scanIdentifier(String string,
                                  int    offset)
    {
        if (offset < 0) {
            return null;
        }
        
        int length = string.length();
        int begin = offset;
        
        while ((offset < length)
               && (this.isIdentifierChar(string.charAt(offset)))) {
            offset++;
        }
        
        if (offset == length) {
            return null;
        }
        else {
            return string.substring(begin, offset);
        }
    }
    
    
    /**
     * Scans one attribute of an object.
     */
    protected int scanOneAttribute(String string,
                                 int    offset)
    {
        String key, value;
        
        key = this.scanIdentifier(string, offset);

        if (key == null) {
            throw new XMLParseException(this.tagName, "Invalid attribute");
        }
        
        offset = this.skipWhiteSpace(string, offset + key.length());
				// removed by <conan@warp.cx> since XML is really case specific
        //key = key.toUpperCase();
        
        if (string.charAt(offset) != '=') {
            String msg = "Attribute " + key + " has no value";
            throw new XMLParseException(this.tagName, msg);
        }
        
        offset = this.skipWhiteSpace(string, offset + 1);
        
        value = this.scanString(string, offset);
        
        if (value == null) {
            String msg = "Attribute " + key + " has no value";
            throw new XMLParseException(this.tagName, msg);
        }
        
        if (value.charAt(0) == '"') {
            value = value.substring(1, (value.length() - 1));
            offset += 2;
        }

        this.attributes.put(key, this.decodeString(value));
        return offset + value.length();    
    }
    
    
    /**
     * Scans a string. Strings are either identifiers, or text delimited by
     * double quotes.
     *
     * @see #scanIdentifier
     */
    protected String scanString(String string,
                              int    offset)
    {
        if (offset < 0) {
            return null;
        }
        
        char delim = string.charAt(offset);
        
        if (delim == '"') {
            int begin = offset;
            int length = string.length();
            offset++;
            
            while ((offset < length) && (string.charAt(offset) != delim)) {
                offset++;
            }
            
            if (offset == length) {
                return null;
            }
            else {
                return string.substring(begin, offset + 1);
            }
        }
        else {
            return this.scanIdentifier(string, offset);
        }
    }
    
    
    /**
     * Scans the class name of the object.
     */
    protected int scanTagName(String string,
                            int    offset)
    {
        this.tagName = this.scanIdentifier(string, offset);

        if (this.tagName == null) {
            throw new XMLParseException(null, "No tag found");
        }
        
				// removed by <conan@warp.cx> since XML is really case specific
        //this.tagName = this.tagName.toUpperCase();
        return offset + this.tagName.length();
    }
    
    
    /**
     * Skips a blah-blah tag.
     */
    protected int skipBogusTag(String string,
                             int    offset)
    {
        if (offset < 0) {
            return -1;
        }

        if (string.startsWith("--", offset + 1)) {
            offset = string.indexOf("-->", offset + 3);

            if (offset > 0) {
                offset += 3;
            }

            return offset;
        }
        
        int level = 1;
        int length = string.length();
        
        while (offset < length) {
            char ch = string.charAt(offset++);
            switch (ch) {
                case '<':
                    level++;
                    break;
                
                case '>':
                    level--;

                    if (level == 0) {
                        return offset;
                    }
                    
                    break;
                    
                default:
            }
        }
        
        return -1;
    }
    
    
    /**
     * Skips the blah-blah tags of the XML definition.
     */
    protected int skipPreamble(String string,
                             int    offset)
    {
        int length = string.length();
        char ch;

        do {
            offset = this.skipWhiteSpace(string, offset);
        
            if ((offset < 0) || (string.charAt(offset) != '<')) {
                return -1;
            }
        
            offset++;
        
            if (offset >= length) {
                throw new XMLParseException(null, "No tag found");
            }
        
            ch = string.charAt(offset);
            
            if ((ch == '!') || (ch == '?')) {
                offset = this.skipBogusTag(string, offset);
            }
        } while (! isIdentifierChar(ch));
        
        return offset;
    }
    
    
    /**
     * Skips whitespace (space, htab, lf and cr).
     */
    protected int skipWhiteSpace(String string,
                               int    offset)
    {
        int length = string.length();
        
        if (offset < 0) {
            return -1;
        }
        
        while ((offset < length)
               && (" \r\n\t".indexOf(string.charAt(offset)) >= 0)) {
            offset++;
        }
     
        if (offset == length) {
            return -1;
        }
        else {
            return offset;
        }
    }


    /**
     * Converts &..; sequences to "normal" chars.
     */
    protected String decodeString(String s)
    {
        StringBuffer result = new StringBuffer(s.length());
        int index = 0;

        for (;;) {
            int index2 = (s + '&').indexOf('&', index);
            result.append(s.substring(index, index2));

            if (index2 == s.length()) {
                break;
            }

            index = s.indexOf(';', index2);
        
            if (index < 0) {
                result.append(s.substring(index2));
                break;
            }

            String key = s.substring(index2 + 1, index);
            
            if (key.charAt(0) == '#') {
                if (key.charAt(1) == 'x') {
                    result.append((char)(Integer.
                                             parseInt(key.substring(2), 16)));
                }
                else {
                    result.append((char)(Integer.
                                             parseInt(key.substring(1), 10)));
                }
            }
            else {
                result.append(this.conversionTable
                                      .getProperty(key, "&" + key + ';'));
            }
            
            index++;
        }

        return result.toString();
    }

	/** Returns the named child, or null if the child does not exist.
	    
			Added 5/30/00 by <conan@warp.cx>
   */
	public XMLElement getChild(String tagName) {
		if (tagName == null) {
			return null;
		}//if
		
		for (Enumeration enum = enumerateChildren(); enum.hasMoreElements(); ) {
			XMLElement e = (XMLElement)enum.nextElement();
			if (tagName.equals(e.getTagName())) {
				return e;
			}//if
		}//for
		
		return null;
	}//getChild

}
