/**
 *  '$RCSfile: ConfigXML.java,v $'
 *  Copyright: 2000 Regents of the University of California and the
 *             National Center for Ecological Analysis and Synthesis
 *    Authors: @authors@
 *    Release: @release@
 *
 *   '$Author: leinfelder $'
 *     '$Date: 2014-06-02 19:16:01 +0000 (Mon, 02 Jun 2014) $'
 * '$Revision: 172 $'
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package edu.ucsb.nceas.utilities.config;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.Hashtable;
import java.util.Vector;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.xpath.XPathAPI;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import edu.ucsb.nceas.utilities.config.exception.ElementNotFoundException;
import edu.ucsb.nceas.utilities.config.exception.IndexTooLargeException;
/**
 * This class is designed to store configuration information in
 * an XML file. The concept is similar to that of a Properties
 * file except that using the XML format allows for a hierarchy
 * of properties and repeated properties.
 *
 * All 'keys' are element names, while values are always stored
 * as XML text nodes. The XML file is parsed and stored in
 * memory as a DOM object.
 *
 * Note that nodes are specified by node tags rather than paths
 */
public class ConfigXML
{
  /**
   * root node of the in-memory DOM structure
   */
  private Node root;
  /**
   * Document node of the in-memory DOM structure
   */
  private Document doc;
  /**
   * XML file name in string form
   */
  private String fileName;
  /**
   * Print writer (output)
   */
  private PrintWriter out;
  private static final String configDirectory = ".morpho";
  /**
   * Set up a DOM parser for reading an XML document
   *
   * @return a DOM parser object for parsing
   */
  private static DocumentBuilder createDomParser() throws Exception
  {
    DocumentBuilder parser = null;
    try
    {
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      factory.setNamespaceAware(true);
      parser = factory.newDocumentBuilder();
      if (parser == null)
      {
        throw new Exception("Could not create Document parser in " +
                            "MonarchUtil.DocumentBuilder");
      }
    }
    catch (ParserConfigurationException pce)
    {
      throw new Exception("Could not create Document parser in " +
                            "MonarchUtil.DocumentBuilder: " + pce.getMessage());
    }
    return parser;
  }
  /**
   * String passed to the creator is the XML config file name
   *
   * @param filename name of XML file
   */
  public ConfigXML(String filename) throws FileNotFoundException, Exception
  {
    this.fileName = filename;
    DocumentBuilder parser = createDomParser();
    File XMLConfigFile = new File(filename);
    InputSource in;
    FileInputStream fs;
    fs = new FileInputStream(filename);
    in = new InputSource(fs);
    try
    {
      doc = parser.parse(in);
      fs.close();
    } catch(Exception e1) {
      throw e1;
    }
    root = doc.getDocumentElement();
  }
   /**
   * String passed to the creator is the XML config file name
   *
   * @param input stream containing the XML configuration data
   */
  public ConfigXML(InputStream configStream) throws FileNotFoundException,
                                                    Exception
  {
    DocumentBuilder parser = createDomParser();
    InputSource in;
    in = new InputSource(configStream);
    try
    {
      doc = parser.parse(in);
      configStream.close();
    } catch(Exception e1) {
      throw e1;
    }
    root = doc.getDocumentElement();
  }
  /**
   * Gets the value(s) corresponding to a key string (i.e. the
   * value(s) for a named parameter.
   *
   * @param key 'key' is element name.
   * @return Returns a Vector of strings because may have repeated elements
   * @throws ElementNotFoundException if key is not found
   */
  public Vector get(String key) throws ElementNotFoundException
  {
    NodeList nl = doc.getElementsByTagName(key);
    Vector result = new Vector();
    if (nl.getLength() < 1)
    {
      throw new ElementNotFoundException("Element " + key + " not found");
    }
    for (int i = 0; i < nl.getLength(); i++)
    {
      Node cn = nl.item(i).getFirstChild(); // assume 1st child is text node
      if ((cn != null) && (cn.getNodeType() == Node.TEXT_NODE))
      {
        String temp = cn.getNodeValue();
        result.addElement(temp.trim());
      }
    }
    return result;
  }
  /**
   * Gets the value(s) corresponding to a key string (i.e. the
   * value(s) for a named parameter.
   *
   * @param key 'key' is element name.
   * @param i zero based index of elements with the name stored in key
   * @return String value of the ith element with name in 'key'
   * @throws ElementNotFoundException if key is not found at position i
   */
  public String get(String key, int i) throws ElementNotFoundException
  {
    NodeList nl = doc.getElementsByTagName(key);
    String result = null;
    if (nl.getLength() < 1)
    {
      throw new ElementNotFoundException("Element " + key +
                                         " not found at position " + i);
    }
    if (nl.getLength() < i)
    {
      throw new ElementNotFoundException("Element " +
                                         key + " not found at position " + i);
    }
    Node cn = nl.item(i).getFirstChild(); // assume 1st child is text node
    if ((cn != null) && (cn.getNodeType() == Node.TEXT_NODE))
    {
      result = (cn.getNodeValue().trim());
    }
    return result;
  }
  /**
   * used to set a value corresponding to 'key'; value is changed
   * in DOM structure in memory
   *
   * @param key 'key' is element name.
   * @param i index in set of elements with 'key' name
   * @param value new value to be inserted in ith key
   * @throws ElementNotFoundException if key is not found at position i
   */
  public void set(String key, int i, String value)
         throws ElementNotFoundException
  {
    boolean result = false;
    NodeList nl = doc.getElementsByTagName(key);
    if (nl.getLength() <= i) {
      throw new ElementNotFoundException("Cannot set key " + key +
                                         " at position " + i +
                                         " either because it does not exist " +
                                         "or because the there are not " + i +
                                         " elements by that name.");
    } else {
      Node cn = nl.item(i).getFirstChild(); // assumed to be a text node
      if (cn == null) {
        // No text node, so append one with the value
        Node newText = doc.createTextNode(value);
        nl.item(i).appendChild(newText);
      } else if (cn.getNodeType() == Node.TEXT_NODE) {
        // found the text node, so change its value
        cn.setNodeValue(value);
      }
    }
  }
  /**
   * Inserts another node before the first element with
   * the name contained in 'key', otherwise appends it
   * to the end of the config file (last element in root node)
   *
   * @param key element name which will be duplicated
   * @param value value for new element
   */
  public void insert(String key, String value)
  {
    // Create the new element, with its text value child
    Node newElem = doc.createElement(key);
    Node newText = doc.createTextNode(value);
    newElem.appendChild(newText);
    // Determine if there are existing elements of the same name
    NodeList nl = doc.getElementsByTagName(key);
    // If so, insert new element before existing
    if (nl.getLength() > 0) {
      Node nnn = nl.item(0);
      Node parent = nnn.getParentNode();
      //insert newElem before nnn
      parent.insertBefore(newElem, nnn);
    // Otherwise, append new element to end of root
    } else {
      root.appendChild(newElem);
    }
  }
  /**
   * Add a sub field to an existing config field
   *
   * @param parentName name of parent element
   * @param i index of parent element
   * @param childName element name of new child
   * @param value value of new child
   * @throws IndexTooLargeException if i is larger than the number of nodes
   *         of parentName
   */
  public void addSubField(String parentName, int i, String childName, String value)
                       throws IndexTooLargeException
  {
    NodeList nl = doc.getElementsByTagName(parentName);
    if (nl.getLength() > 0)
    {
      if (nl.getLength() <= i)
      {
        throw new IndexTooLargeException("Error setting XMLConfig value: " +
                                         "index too large");
      }
      else
      {
        Node parent = nl.item(i);
        Node newElem = doc.createElement(childName);
        Node newText = doc.createTextNode(value);
        //add text to element
        newElem.appendChild(newText);
        //add newElem to parent
        parent.appendChild(newElem);
      }
    }
  }
  /**
   * deletes indicated field
   *
   * @param nodeName field to delete
   * @param i node index
   * @throws IndexTooLargeException if i is larger than the number of nodeName
   * nodes.
   */
  public void delete(String nodeName, int i) throws IndexTooLargeException
  {
    NodeList nl = doc.getElementsByTagName(nodeName);
    if (nl.getLength() > 0)
    {
      if (nl.getLength() <= i)
      {
        throw new IndexTooLargeException("Error removing XMLConfig value: " +
                                         "index too large");
      }
      else
      {
        Node nnn = nl.item(i);
        Node parent = nnn.getParentNode();
        parent.removeChild(nnn);
      }
    }
  }
  /**
   * removes all subfields within specified super field
   *
   * @param parentName the super field
   * @param i index of super field
   */
  public void deleteSubFields(String parentName, int i)
         throws IndexTooLargeException
  {
    NodeList nl = doc.getElementsByTagName(parentName);
    if (nl.getLength() > 0)
    {
      if (nl.getLength() <= i)
      {
        throw new IndexTooLargeException("Error setting XMLConfig value: " +
                                 "index too large");
      }
      else
      {
        Node parent = nl.item(i);
        NodeList nlchildren = parent.getChildNodes();
        int numchildren = nlchildren.getLength();
        for (int k = 0; k < numchildren; k++)
        {
          Node temp = nlchildren.item(0);
          parent.removeChild(temp);
        }
      }
    }
  }
  /**
   * Assume that there is some parent node which has a subset of
   * child nodes that are repeated e.g.
   * 
   *    xxx
   *    qqq
   *    yyy
   *    www
   *    ...
   * 
   *
   * this method will return a Hashtable of names-values of parent
   * @param field the name of the field in which the name/value pair resides
   * @param name the name sub field in the name/value pair
   * @param value the value sub field in the name/value pair
   * @throws ElementNotFoundException if the parentName field is not found
   */
  public Hashtable getNameValuePairs(String field, String name,
                                     String value)
                                     throws ElementNotFoundException
  {
    try
    {
      Hashtable h = getNameValuePairs(field, name, value, 0);
      return h;
    }
    catch(IndexTooLargeException e)
    {
      throw new ElementNotFoundException("The element " + field +
                                         " does not exist.");
    }
  }
  /**
   * Assume that there is some parent node which has a subset of
   * child nodes that are repeated e.g.
   * 
   *    xxx
   *    qqq
   *    yyy
   *    www
   *    ...
   * 
   *
   * this method will return a Hashtable of names-values of parent
   * @param field the name of the parent field of the name/value pair
   * @param name the name node in the name/value pair
   * @param value the value node in the name/value pair
   * @param i the index of the parent that you want in the node list.
   * @throws IndexTooLargeException if there are less than i parentName nodes
   */
  public Hashtable getNameValuePairs(String field, String name,
                                     String value, int i)
                                     throws IndexTooLargeException
  {
    String keyval = "";
    String valval = "";
    Hashtable ht = new Hashtable();
    NodeList nl = doc.getElementsByTagName(field);
    if (nl.getLength() > 0)
    {
      if(nl.getLength() <= i)
      {
        throw new IndexTooLargeException("There are not " + i + " nodes in " +
                                         "the resultset.");
      }
      // always use the first parent
      NodeList children = nl.item(i).getChildNodes();
      if (children.getLength() > 0)
      {
        for (int j = 0; j < children.getLength(); j++)
        {
          Node cn = children.item(j);
          if ((cn.getNodeType() == Node.ELEMENT_NODE)
              && (cn.getNodeName().equalsIgnoreCase(name)))
          {
            Node ccn = cn.getFirstChild();        // assumed to be a text node
            if ((ccn != null) && (ccn.getNodeType() == Node.TEXT_NODE))
            {
              keyval = ccn.getNodeValue();
            }
          }
          if ((cn.getNodeType() == Node.ELEMENT_NODE)
              && (cn.getNodeName().equalsIgnoreCase(value)))
          {
            Node ccn = cn.getFirstChild();        // assumed to be a text node
            if ((ccn != null) && (ccn.getNodeType() == Node.TEXT_NODE))
            {
              valval = ccn.getNodeValue();
              ht.put(keyval, valval);
            }
          }
        }
      }
    }
    return ht;
  }
  /**
   *  utility routine to return the value(s) of a node defined by
   *  a specified XPath
   * @param pathstring the path to the requested nodes
   */
  public Vector getValuesForPath(String pathstring)
         throws ElementNotFoundException, Exception
  {
    Vector val = new Vector();
    if (!pathstring.startsWith("/"))
    {
      pathstring = "//*/"+pathstring;
    }
    try
    {
      NodeList nl = null;
      nl = XPathAPI.selectNodeList(doc, pathstring);
      if ((nl!=null)&&(nl.getLength()>0))
      {
        // loop over node list is needed if node is repeated
        for (int k=0;k");
      print(nd);
      out.close();
    }
  }
  /**
   * This method can 'print' any DOM subtree. Specifically it is
   * set (by means of 'out') to write the in-memory DOM to the
   * same XML file that was originally read. Action thus saves
   * a new version of the XML doc
   *
   * @param node node usually set to the 'doc' node for complete XML file
   * re-write
   */
  private void print(Node node)
  {
    // is there anything to do?
    if (node == null)
    {
      return;
    }
    int type = node.getNodeType();
    switch (type)
    {
      // print document
    case Node.DOCUMENT_NODE:
    {
      out.println("");
      print(((Document) node).getDocumentElement());
      out.flush();
      break;
    }
      // print element with attributes
    case Node.ELEMENT_NODE:
    {
      out.print('<');
      out.print(node.getNodeName());
      Attr attrs[] = sortAttributes(node.getAttributes());
      for (int i = 0; i < attrs.length; i++)
      {
        Attr attr = attrs[i];
        out.print(' ');
        out.print(attr.getNodeName());
        out.print("=\"");
        out.print(normalize(attr.getNodeValue()));
        out.print('"');
      }
      out.print('>');
      NodeList children = node.getChildNodes();
      if (children != null)
      {
        int len = children.getLength();
        for (int i = 0; i < len; i++)
        {
          print(children.item(i));
        }
      }
      break;
    }
      // handle entity reference nodes
    case Node.ENTITY_REFERENCE_NODE:
    {
      out.print('&');
      out.print(node.getNodeName());
      out.print(';');
      break;
    }
      // print cdata sections
    case Node.CDATA_SECTION_NODE:
    {
      out.print("");
      break;
    }
      // print text
    case Node.TEXT_NODE:
    {
      out.print(normalize(node.getNodeValue()));
      break;
    }
      // print processing instruction
    case Node.PROCESSING_INSTRUCTION_NODE:
    {
      out.print("");
      out.print(node.getNodeName());
      String data = node.getNodeValue();
      if (data != null && data.length() > 0)
      {
        out.print(' ');
        out.print(data);
      }
      out.print("?>");
      break;
    }
    }
    if (type == Node.ELEMENT_NODE)
    {
      out.print("");
      out.print(node.getNodeName());
      out.print('>');
    }
    out.flush();
  }
  /** Returns a sorted list of attributes. */
  private Attr[] sortAttributes(NamedNodeMap attrs)
  {
    int len = (attrs != null) ? attrs.getLength() : 0;
    Attr array[] = new Attr[len];
    for (int i = 0; i < len; i++)
    {
      array[i] = (Attr) attrs.item(i);
    }
    for (int i = 0; i < len - 1; i++)
    {
      String name = array[i].getNodeName();
      int index = i;
      for (int j = i + 1; j < len; j++)
      {
        String curName = array[j].getNodeName();
        if (curName.compareTo(name) < 0)
        {
          name = curName;
          index = j;
        }
      }
      if (index != i)
      {
        Attr temp = array[i];
        array[i] = array[index];
        array[index] = temp;
      }
    }
    return (array);
  } // sortAttributes(NamedNodeMap):Attr[]
  /** Normalizes the given string. */
  private String normalize(String s)
  {
    StringBuffer str = new StringBuffer();
    int len = (s != null) ? s.length() : 0;
    for (int i = 0; i < len; i++)
    {
      char ch = s.charAt(i);
      switch (ch)
      {
      case '<':
      {
        str.append("<");
        break;
      }
        case '>':
      {
        str.append(">");
        break;
      }
      case '&':
      {
        str.append("&");
        break;
      }
      case '"':
      {
        str.append(""");
        break;
      }
      case '\r':
      case '\n':
      {
        // else, default append char
      }
      default:
      {
        str.append(ch);
      }
      }
    }
    return (str.toString());
  }
  /**
   * Determine the home directory in which configuration files should be located
   *
   * @return String name of the path to the configuration directory
   */
  public static String getConfigDirectory() {
    return System.getProperty("user.home") + File.separator + configDirectory;
  }
}