// package xmlutils;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.io.IOException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Element;
import org.w3c.dom.Document;
import org.w3c.dom.Text;
import org.xml.sax.SAXException;

/**
 * DOM represents an XML source at a rather low level, this class represents it at a higher level.
 * It is a layer on top of DOM. It considers an XML source to consist of a tree of XML elements only.
 * Each having the following properties only: name, value (corresponding to only one org.w3c.dom.Text node),
 * attributes list, child elements list.
 * This class represents such an XML element.
 * It is suited for simple parsing of an XML source containing structured data, like system properties,
 * saved objects, etc.
 * Because it can have only one Text node it is not suited for parsing an XML source containing
 * mixed content XML elements, like structured text files.
 * <p>
 * NB.1. Since Java 1.4 standard includes a DOM parser, this class is all you need. The whole package/libary
 * consists of this class only.
 * <p>
 * NB.2. The jar file of this class is even smaller than its NanoXml (http://nanoxml.n3.net) counterpart.
 * And maybe it is even simpler to use.
 *
 * @author Ronald Koster
 * @version 0.9
 * @see <a href="http://www.ronaldkoster.net" target="_top">Ronald Koster - Home Page</a>
 */
public class XmlElement {
   private String name;
   private String value;
   private HashMap attributes;
   private List childElements;

   public String getName() { return name; }

   public void setName(String aName) { name = aName; }

   public String getValue() { return value; }

   public void setValue(String aValue) { value = aValue; }

   public HashMap getAttributes() { return attributes; }

   public void setAttributes(HashMap aAttributes) {attributes = aAttributes; }

   public List getChildElements() { return childElements; }

   public void setChildElements(List aList) { childElements = aList; }

   /**
    * Gets attribute with name aName.
    * Encapsulates attributes#get.
    * @param aName See above.
    * @return Idem.
    */
   public String getAttribute(String aName) {
      if (attributes == null) { return null; }
      return (String) attributes.get(aName);
   }


   /**
    * Sets/Adds an attribute. Set if it already exists, else add.
    * Encapsulates attributes#put.
    * @param aName Attribute name.
    * @param aValue Attribute value.
    * @return If atttribute already existed, the old value, else null.
    */
   public String setAttribute(String aName, String aValue) {
      if (attributes == null) { attributes = new HashMap(); }
      return (String) attributes.put(aName, aValue);
   }


   /**
    * Removes an attribute.
    * Encapsulates attributes#remove.
    * @param aName Attribute name.
    * @return Value of removed attribute.
    */
   public String removeAttribute(String aName) {
      if (attributes == null) { return null; }
      return (String) attributes.remove(aName);
   }


   /**
    * Gets child element with index aIndex.
    * Encapsulates childElements#get(int).
    * @param aIndex See above.
    */
   public XmlElement getChildElement(int aIndex) {
      if (childElements == null || aIndex < 0 || aIndex >= childElements.size()) { return null; }
      return (XmlElement) childElements.get(aIndex);
   }

   /**
    * Get first occurence of child element with name aName.
    * @param aName See above.
    */
   public XmlElement getChildElement(String aName) { return getChildElement(aName, 0); }

   /**
    * Get aIndex-th occurence of child element with name aName.
    * @param aName See above.
    * @param aIndex Idem.
    */
   public XmlElement getChildElement(String aName, int aIndex) {
      if (childElements == null || aName == null|| aIndex < 0 || aIndex >= childElements.size()) { return null; }

      int counter = -1;
      for (int i = 0; i < childElements.size(); i++) {
         XmlElement elem = getChildElement(i);
         if (aName.equals(elem.getName())) {
            counter++;
         }
         if (counter == aIndex) {
            return elem;
         }
      }

      return null;
   }


   /**
    * Appends a child element to this element's child elements list.
    * Encapsulates childElements#add(Object).
    * @param aChild See above.
    */
   public void addChildElement(XmlElement aChild) {
      if (childElements == null) { childElements = new ArrayList(); }
      childElements.add(aChild);
   }


   /**
    * Inserts a child element to this element's child elements list at position aIndex.
    * Encapsulates childElements#add(int, Object).
    * @param aIndex See above.
    * @param aChild Idem.
    */
   public void addChildElement(int aIndex, XmlElement aChild) {
      if (childElements == null) { childElements = new ArrayList(); }
      childElements.add(aIndex, aChild);
   }


   /**
    * Removes child element at postion aIndex from this element's child elements list.
    * Encapsulates childElements#remove(int).
    * @param aIndex See above.
    */
   public XmlElement removeChildElement(int aIndex) {
      if (childElements == null) { return null; }
      return (XmlElement) childElements.remove(aIndex);
   }

   /**
    * Creates an XmlElement from aNode. Includes recursively all relevant child nodes if any.
    * <p>
    * NB. Only limited support for mixed content elements. For each node only the first org.w3c.dom.Text
    * node is copied to XmlElement#value field. Other Text nodes are ignored.
    * @param aNode Must be of type org.w3c.dom.Document or org.w3c.dom.Element.
    * @return XmlElement created.
    * @throws ClassCastException if aNode is of invalid type.
    * @throws NullPointeException if <tt>aNode == null</tt>.
    */
   public static XmlElement createFromNode(Node aNode) {
      Element node;
      if (aNode instanceof Document) {
         node = ((Document) aNode).getDocumentElement();
      }
      else {
         node = (Element) aNode;
      }
      NodeList children = node.getChildNodes();

      XmlElement elem = new XmlElement();

      // Name.
      elem.setName(node.getNodeName());

      // Value.
      if (children != null) {
         for (int i = 0; i < children.getLength(); i++) {
            if (children.item(i) instanceof Text) {
               String s = children.item(i).getNodeValue();
               elem.setValue(s);
            }
            break;
         }
      }

      // Attributes.
      NamedNodeMap atts = node.getAttributes();
      if (atts != null) {
         for (int i = 0; i < atts.getLength(); i++) {
            String attName = atts.item(i).getNodeName();
            String attValue = atts.item(i).getNodeValue();
            if (attValue != null) { elem.setAttribute(attName, attValue); }
         }
      }

      // Child nodes.
      if (children != null) {
         for (int i = 0; i < children.getLength(); i++) {
            if (children.item(i) instanceof Element) {
               elem.addChildElement(createFromNode(children.item(i)));  // NB. Recursive invokation!
            }
         }
      }

      return elem;
   }


   /**
    * Creates a org.w3c.org.Element from this XmlElement.
    * @return See above.
    * @throws SAXException if conversion failes.
    * @throws ParserConfigurationException idem.
    * @throws IOException idem.
    */
   public Element toNode() throws SAXException, ParserConfigurationException, IOException {
      String xmlString = toString();
      if (xmlString == null) { return null; }

      ByteArrayInputStream is = new ByteArrayInputStream(xmlString.getBytes());
      DocumentBuilderFactory fac = DocumentBuilderFactory.newInstance();
      DocumentBuilder builder = fac.newDocumentBuilder();
      Document doc = builder.parse(is);
      return doc.getDocumentElement();
   }

   /**
    * Converts this class to an XML string. Excluding the &lt;?xml... and &lt;!DOCTYPE... stuff.
    * <p>
    * NB. Does not support the occurence of double and single qoutes simultaniously in the value field of
    * itself or one of its child nodes. In that cases the XML string returned will not be syntactically
    * correct.
    * @return See above.
    *
    */
   public String toString() {
      String str = '<' + getName();

      // Attributes.
      if (attributes != null) {
         Iterator it = attributes.keySet().iterator();
         while (it.hasNext()) {
            Object attName = it.next();
            Object attValue = attributes.get(attName);

            // Default use double qoutes as outer qoutes. However, if double quotes are contained in de
            // data use single quotes. If both types of quotes occur in the data following appraoch failes
            // to produce correct XML. However, since such data should be rare this is left as it is.
            char quote = '"'; // Default double quotes.
            if (attValue.toString().indexOf(quote) > -1) { quote = '\''; } // Else single qoutes.
            str += ' ' + attName.toString() + '=' + quote + attValue + quote;
         }
      }

      // Value.
      String s = getValue();
      if (s == null) { s = ""; }
      str += '>' + s;

      // Child elements.
      if (childElements != null) {
         for (int i = 0; i < childElements.size(); i++) {
            str += getChildElement(i).toString(); // NB. Recursive invokation!
         }
      }

      str += "</" + getName() + '>';

      return str;
   }


   /**
    * For testing puposes only. Converts a given XML file to an XmlElement object which is then
    * converted to XML again via {@link #toString()}. Output is written to stdout.
    * Compare the result with the input file to check correct functioning of this class.
    * @param args See {@link #printUsage}.
    */
   public static void main(String[] args) {
      if (args == null || args.length != 1) {
         printUsage();
         System.exit(0);
      }

      try {
         DocumentBuilderFactory fac = DocumentBuilderFactory.newInstance();
         DocumentBuilder builder = fac.newDocumentBuilder();
         Document doc = builder.parse(new File(args[0]));

         XmlElement elem = XmlElement.createFromNode(doc);
         System.out.println(elem.toString());
      }
      catch (Exception ex) {
         System.out.println("Unexpected excpetion: " + ex);
      }
   }


   public static void printUsage() {
      System.out.println("Usage: java <XML filename>");
   }
}

