content: /* The contents of this file are subject to the terms
   * of the Common Development and Distribution License
   * (the License). You may not use this file except in
   * compliance with the License.
   *
   * You can obtain a copy of the License at
   * http://www.sun.com/cddl/cddl.html or
   * install_dir/legal/LICENSE
   * See the License for the specific language governing
   * permission and limitations under the License.
   *
   * When distributing Covered Code, include this CDDL
   * Header Notice in each file and include the License file
   * at install_dir/legal/LICENSE.
   * If applicable, add the following below the CDDL Header,
   * with the fields enclosed by brackets [] replaced by
   * your own identifying information:
   * "Portions Copyrighted [year] [name of copyright owner]"
   *
   * $Id$
   *
   * Copyright 2005-2009 Sun Microsystems Inc. All Rights Reserved
   */
  package com.sun.faban.harness.util;
  
  import com.sun.faban.common.NameValuePair;
  import com.sun.faban.common.ParamReader;
  import com.sun.faban.common.Utilities;
  import org.w3c.dom.Document;
  import org.w3c.dom.Element;
  import org.w3c.dom.Node;
  import org.w3c.dom.NodeList;
  
  import javax.xml.parsers.DocumentBuilder;
  import javax.xml.parsers.DocumentBuilderFactory;
  import javax.xml.transform.OutputKeys;
  import javax.xml.transform.Transformer;
  import javax.xml.transform.TransformerFactory;
  import javax.xml.transform.dom.DOMSource;
  import javax.xml.transform.stream.StreamResult;
  import javax.xml.xpath.XPath;
  import javax.xml.xpath.XPathConstants;
  import javax.xml.xpath.XPathExpressionException;
  import javax.xml.xpath.XPathFactory;
  import java.io.*;
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.LinkedHashSet;
  import java.util.List;
  import java.util.logging.Level;
  import java.util.logging.Logger;
  
  /**
   * Provides all XML accesses to the parameter repository and other XML files.
   */
  public class XMLReader {
  
      private static Logger logger = Logger.getLogger(XMLReader.class.getName());
      private Document doc;
      private XPath xPath;
      private String file;
      private boolean updated = false;
      private HashMap<Node, ArrayList<NameValuePair<Integer>>> hostPortsTable;
  
      /**
       * Constructs the XML reader.
       * @param file The XML file
       */
      public XMLReader(String file) {
          initLocal(file);
      }
  
      /**
       * Constructs the XML reader.
       * @param file The XML file
       * @param useFabanNS Whether to apply Faban namespaces in this file
       * @param warnDeprecated Whether to warn for deprecated Faban elements
       */
      public XMLReader(String file, boolean useFabanNS, boolean warnDeprecated) {
          if (useFabanNS)
              try {
                  this.file = file;
                  ParamReader reader = new ParamReader(file, warnDeprecated);
                  doc = reader.getDocument();
                  xPath = reader.getXPath();
              } catch (Exception e) {
                  throw new XMLException(e.getMessage(), e);
              }
          else
              initLocal(file);
      }
  
      private String completeXPath(String xpath) {
          // If no absolute xpath is not given use //xpath to find the parameter
          if (xpath.charAt(0) != '/') {
              xpath = "//" + xpath;
          } else {
              //the JXPathContext expects 'params' (which is the variable name returned by XMLFile
              xpath = "params" + xpath;
          }
          return xpath;
      }
  
      private void initLocal(String file) {
          this.file = file;
          try {
              DocumentBuilder builder = DocumentBuilderFactory.newInstance().
                      newDocumentBuilder();
              doc = builder.parse(new File(file));
              xPath = XPathFactory.newInstance().newXPath();
          } catch (Exception e) {
              throw new XMLException(e.getMessage(), e);
          }
      }
  
      /**
       * Returns nodelist for the given tag.
       * @param tagName
       * @return nodelist
       */
      public NodeList getNodeListForTagName(String tagName){
          return doc.getElementsByTagName(tagName);
      }
  
      /**
       * Returns nodelist for top level elements(Children of root element).
       * @return nodelist.
       */
      public NodeList getTopLevelElements() {
          NodeList topLevelElements = doc.getDocumentElement().getChildNodes();
          return topLevelElements;
      }
  
      /**
       * Returns the root element.
       * @return element.
       */
      public Element getRootNode() {
          return doc.getDocumentElement();
      }
  
      /**
       * Returns node for the given xpath.
       * @param xpath
       * @return node
       */
      public Node getNode(String xpath) {
          xpath = completeXPath(xpath);
          return getNode(xpath, doc);
      }
  
      /**
       * Returns node for the given xpath based on the base node.
       * @param xpath
       * @param base
       * @return node.
       */
      public Node getNode(String xpath, Node base) {
          Node node = null;
          try {
              node = (Node) xPath.evaluate(xpath,
                                                  base, XPathConstants.NODE);
          } catch (XPathExpressionException e) {
              logger.log(Level.WARNING, "Error processing XPath expression: " +
                                                                  xpath, e);
          }
          return node;
      }
  
      /**
       * Returns the nodelist for a given xpath.
       * @param xpath
       * @return nodelist.
       */
      public NodeList getNodes(String xpath) {
          xpath = completeXPath(xpath);
          return getNodes(xpath, doc);
      }
  
      /**
       * Returns the nodelist for a given xpath based on a base node.
       * @param xpath
       * @param base
       * @return nodelist.
       */
      public NodeList getNodes(String xpath, Node base) {
          NodeList nodes = null;
          try {
              nodes = (NodeList) xPath.evaluate(xpath,
                                                  base, XPathConstants.NODESET);
          } catch (XPathExpressionException e) {
              logger.log(Level.WARNING, "Error processing XPath expression: " +
                                                                  xpath, e);
          }
          return nodes;
      }
  
      /**
       * Returns the value for the given xpath.
       * @param xpath
       * @return string.
       */
      public String getValue(String xpath) {
          xpath = completeXPath(xpath);
          return getValue(xpath, doc);
      }
  
      /**
       * Returns the value for the given xpath based on the base node.
       * @param xpath
       * @param base
       * @return string.
       */
      public String getValue(String xpath, Node base) {
          try {
              return xPath.evaluate(xpath, base);
          } catch (XPathExpressionException e) {
              throw new XMLException("Error evaluating " + xpath + ", " +
                                     e.getMessage(), e);
          }
      }
  
      /**
       * Returns the list of values for a given xpath.
       * @param xpath
       * @return List<String>.
       */
      public List<String> getValues(String xpath) {
          xpath = completeXPath(xpath);
          return getValues(xpath, doc);
      }
  
      /**
       * Returns the list of values for a given xpath based on base node.
       * @param xpath
       * @param base
       * @return List<String>.
       */
      public List<String> getValues(String xpath, Node base) {
          try {
              NodeList nodeList = (NodeList) xPath.evaluate(
                                  xpath, base, XPathConstants.NODESET);
              int length = nodeList.getLength();
              ArrayList<String> vList = new ArrayList<String>();
              for (int i = 0; i < length; i++) {
                  Node node = nodeList.item(i);
                  String value = null;
                  nodeTraceLoop:
                  while (node != null)
                      switch (node.getNodeType()) {
                          case Node.ATTRIBUTE_NODE :
                          case Node.CDATA_SECTION_NODE :
                          case Node.COMMENT_NODE :
                          case Node.PROCESSING_INSTRUCTION_NODE :
                          case Node.TEXT_NODE : value = node.getNodeValue();
                                                break nodeTraceLoop;
                          default : node = node.getFirstChild();
                      }
  
                  if (value == null)
                      value = "";
                  vList.add(value);
              }
              return vList;
          } catch (XPathExpressionException e) {
              throw new XMLException("Error evaluating " + xpath + ", " +
                                     e.getMessage(), e);
          }
      }
  
      /**
       * Returns the list of attribute values for a given xpath and attribute.
       * @param xpath
       * @param attribute
       * @return List<String>.
       */
      public List<String> getAttributeValues(String xpath, String attribute) {
          xpath = completeXPath(xpath);
          return getAttributeValues(xpath, attribute, doc);
      }
  
      /**
       * Returns the list of attribute values for a given xpath and attribute
       * based on base node.
       * @param xpath
       * @param attribute
       * @param base
       * @return List<String>.
       */
      public List<String> getAttributeValues(String xpath, String attribute, Node base) {
          try {
              NodeList nodeList = (NodeList) xPath.evaluate(xpath + "[@" + attribute + "]",
                                  base, XPathConstants.NODESET);
              int length = nodeList.getLength();
              ArrayList<String> vList = new ArrayList<String>();
              for (int i = 0; i < length; i++) {
                  Node node = nodeList.item(i);
                  String value = null;
                  nodeTraceLoop:
                  while (node != null)
                      switch (node.getNodeType()) {
                          case Node.ELEMENT_NODE:
                              Element e = (Element) node;
                              value = e.getAttribute(attribute);
                              break nodeTraceLoop;
                          case Node.ATTRIBUTE_NODE :
                              value = node.getNodeValue();
                              break nodeTraceLoop;
                          default : node = node.getFirstChild();
                      }
  
                  if (value == null)
                      value = "";
                  vList.add(value);
              }
              return vList;
          } catch (XPathExpressionException e) {
              throw new XMLException("Error evaluating " + xpath + ", " +
                                     e.getMessage(), e);
          }
      }
  
      /**
       * Sets the new value for a given xpath.
       * @param xpath The XPath to the element to set the value
       * @param newValue The new value
       */
      public void setValue(String xpath, String newValue) {
          // If no absolute xpath is not given use //xpath to find the parameter
          xpath = completeXPath(xpath);
          
          try {
              NodeList nodeList = (NodeList) xPath.evaluate(
                                  xpath, doc, XPathConstants.NODESET);
              int length = nodeList.getLength();
              boolean updated = false;
              for (int i = 0; i < length; i++) {
                  Node node = nodeList.item(i);
                  nodeTraceLoop:
                  while (node != null) {
                      short nodeType = node.getNodeType();
                      logger.finer("XPath: " + xpath);
                      logger.finer("NodeType[" + i + "]: " + nodeType);
  
                      switch (nodeType) {
                          case Node.ATTRIBUTE_NODE :
                          case Node.CDATA_SECTION_NODE :
                          case Node.COMMENT_NODE :
                          case Node.PROCESSING_INSTRUCTION_NODE :
                          case Node.TEXT_NODE : node.setNodeValue(newValue);
                                                updated = true;
                                                break nodeTraceLoop;
                          default :   if (node.hasChildNodes()) {
                                          node = node.getFirstChild();
                                      } else {
                                          node.setTextContent(newValue);
                                          updated = true;
                                          break nodeTraceLoop;
                                      }
                      }
                  }
              }
              if (updated)
                  this.updated = true;
              else
                  throw new XMLException("Update of XPath " + xpath +
                          " unsuccessful!");
          } catch (XPathExpressionException e) {
              throw new XMLException("Error evaluating " + xpath + ", " +
                                     e.getMessage(), e);
          }
      }
  
      /**
       * Sets the new value for the element.
       * @param element The DOM element
       * @param value The new value
       */
      public void setValue(Element element, String value) {
          NodeList children = element.getChildNodes();
          boolean valueSet = false;
          int childCount = children.getLength();
          for (int i = 0; i < childCount; i++) {
              Node child = children.item(i);
              short nodeType = child.getNodeType();
              if (nodeType == Node.TEXT_NODE) {
                  child.setNodeValue(value);
                  valueSet = true;
                  break;
              }
          }
          if (!valueSet)
              element.appendChild(doc.createTextNode(value));
      }
      
      /**
       * Replaces the new value for the element text.
       * @param element
       * @param value
       */
      public void replaceValue(Element element, String value) {
          NodeList children = element.getChildNodes();
          int childCount = children.getLength();
          for (int i = 0; i < childCount; i++) {
              Node child = children.item(i);
              short nodeType = child.getNodeType();
              if (nodeType == Node.TEXT_NODE) {
                  child.setTextContent(value);
                  this.updated = true;
                  break;
              }
          }
      }
  
      /**
       * Deletes a node.
       * @param deleteNode
       * @param parent
       * @return Element
       */
      public void deleteNode(Node deleteNode, Node parent) {
          parent.removeChild(deleteNode);
          this.updated = true;
      }
  
      /**
       * Adds a node.
       * @param parent
       * @param namespaceURI
       * @param prefix
       * @param nodeName
       * @return Element
       */
      public Element addNode(Element parent, String namespaceURI,
                             String prefix, String nodeName) {
          Element newNode;
          if (namespaceURI == null) {
              newNode = doc.createElement(nodeName);
          } else {
              newNode = doc.createElementNS(namespaceURI, nodeName);
              if (prefix != null)
                  newNode.setPrefix(prefix);
          }
          parent.appendChild(newNode);
          this.updated = true;
          return newNode;
      }
  
      /**
       * Adds a node.
       * @param baseXPath
       * @param namespaceURI
       * @param prefix
       * @param nodeName
       * @return Element
       */
      public Element addNode(String baseXPath, String namespaceURI,
                             String prefix, String nodeName) {
          // If no absolute baseXPath is not given use //baseXPath to find the parameter
          if(baseXPath.charAt(0) != '/')
              baseXPath = "//" + baseXPath;
          else    //the JXPathContext expects 'params' (which is the variable name returned by XMLFile
              baseXPath = "params" + baseXPath;
          try {
              NodeList nodeList = (NodeList) xPath.evaluate(
                                  baseXPath, doc, XPathConstants.NODESET);
              int length = nodeList.getLength();
              if (length == 0) {
                  logger.warning("No match for XPath " + baseXPath);
                  return null;
              } else if (length > 1) {
                  logger.warning("XPath " + baseXPath +
                          " references more than one node. Please make the " +
                          "XPath more specific.");
                  return null;
              }
  
              Node node = nodeList.item(0);
              if (node.getNodeType() != Node.ELEMENT_NODE) {
                  logger.warning("XPath " + baseXPath +
                          " does not reference an element.");
                  return null;
              }
  
              return addNode((Element) node, namespaceURI, prefix, nodeName);
              
          } catch (XPathExpressionException e) {
              throw new XMLException("Error evaluating " + baseXPath + ", " +
                                     e.getMessage(), e);
          }
  
      }
  
      /**
       * This method saves the XML file if it was modified
       * and if a back up file name is specified the original
       * file is backed up.
       * @param backupFileName  - the name of the backup file
       * @return Whether the save succeeded
       * @throws Exception Error saving the file
       */
      public boolean save(String backupFileName) throws Exception {
  
          // Check if a save is needed
          if(!updated)
              return false;
  
          //backup the file
          if(backupFileName != null) {
              BufferedReader in = new BufferedReader(new FileReader(file));
              BufferedWriter out = new BufferedWriter(new FileWriter(backupFileName));
              int ch;
              while((ch = in.read()) != -1)
                  out.write(ch);
              in.close();
              out.close();
          }
  
          TransformerFactory tFactory = TransformerFactory.newInstance();
          Transformer transformer = tFactory.newTransformer();
          // transformer.getOutputProperties().list(System.out);
  
          if (doc.getDoctype() != null){
              transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, doc.getDoctype().getPublicId());
              transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, doc.getDoctype().getSystemId());
          }
  
          DOMSource source = new DOMSource(doc);
          StreamResult result = new StreamResult(new File(file).getAbsolutePath());
          transformer.transform(source, result);
  
          return true;
      }
  
      /**
       * Scans the XML for specific entries matching
       * "//fa:hostConfig/fa:hostPorts" and processes them for later retrieval.
       */
      public void processHostPorts() {
          String xPathExpr = "//fa:hostConfig/fa:hostPorts";
          hostPortsTable = new HashMap<Node, ArrayList<NameValuePair<Integer>>>();
          try {
              NodeList nodeList = (NodeList) xPath.evaluate(xPathExpr, doc,
                                                          XPathConstants.NODESET);
              int entries = nodeList.getLength();
              for (int i = 0; i < entries; i++) {
                  processHostPorts(nodeList.item(i));
              }
  
          } catch (XPathExpressionException e) {
              logger.log(Level.WARNING, "Error processing XPath expression: " +
                                                                  xPathExpr, e);
          }
      }
  
      private boolean isHostNode(Node hostsNode) {
          if (hostsNode.getNodeType() != Node.ELEMENT_NODE)
              return false;
          String nodeName = hostsNode.getNodeName();
          if (nodeName == null)
              return false;
          if (!nodeName.endsWith(":host"))
              return false;
          if (!ParamReader.FABANURI.equals(hostsNode.getNamespaceURI()))
              return false;
          return true;
      }
  
      private void processHostPorts(Node hostPortNode) {
  
          Node hostConfig = hostPortNode.getParentNode();
          Node firstChild = hostConfig.getFirstChild();
          Node hostsNode = firstChild;
          if (!isHostNode(hostsNode)) {
              hostsNode = null;
              NodeList hostConfigNodes = hostConfig.getChildNodes();
              int length = hostConfigNodes.getLength();
              for (int i = 1; i < length; i++) {
                  Node pHost = hostConfigNodes.item(i);
                  if (isHostNode(pHost)) {
                      hostsNode = pHost;
                      break;
                  }
              }
          }
  
          LinkedHashSet<String> hostsSet = new LinkedHashSet<String>();
  
          if (hostsNode != null) {
              Node textNode = hostsNode.getFirstChild();
              if (textNode != null && textNode.getNodeType() == Node.TEXT_NODE) {
                  String[] hostList = textNode.getNodeValue().split("\\s");
                  for (String hostName : hostList) {
                      hostsSet.add(hostName);
                  }
              }
          }
  
          Node clientsNode = hostPortNode.getFirstChild();
          String clients;
          if (clientsNode == null) {
              clients = "";
          } else {
              clients = clientsNode.getNodeValue();
              if (clients == null)
                  clients = "";
          }
  
          ArrayList<NameValuePair<Integer>> hostsPorts =
                                      new ArrayList<NameValuePair<Integer>>();
  
          String hosts = Utilities.parseHostPorts(clients, hostsPorts, hostsSet);
  
          if (hostsNode == null) {
              hostsNode = doc.createElementNS(ParamReader.FABANURI, "host");
              hostsNode.setPrefix("fa");
              hostsNode.appendChild(doc.createTextNode(hosts.toString().trim()));
              hostConfig.insertBefore(hostsNode, firstChild);
          } else {
              Node textNode = hostsNode.getFirstChild();
              if (textNode != null)
                  textNode.setNodeValue(hosts.toString().trim());
              else
                  hostsNode.appendChild(doc.createTextNode(
                          hosts.toString().trim()));                        
          }
          hostPortsTable.put(hostPortNode, hostsPorts);
      }
  
      /**
       * Obtains the host:port name value pair list from the element
       * matching this XPath.
       * @param xPathExpr
       * @return The list of host:port elements, or null if the XPath does
       * not exist or does not point to a host:port node.
       */
      public List<NameValuePair<Integer>> getHostPorts(String xPathExpr) {
          ArrayList<NameValuePair<Integer>> hostsPorts = null;
          if(xPathExpr.charAt(0) != '/')
              xPathExpr = "//" + xPathExpr;
          else    //the JXPathContext expects 'params' which is the variable name returned by XMLFile
              xPathExpr = "params" + xPathExpr;
  
          try {
              Node hostPortNode = (Node) xPath.evaluate(xPathExpr,
                                                  doc, XPathConstants.NODE);
              if (hostPortNode != null)
                  hostsPorts = hostPortsTable.get(hostPortNode);
  
          } catch (XPathExpressionException e) {
              logger.log(Level.WARNING, "Error processing XPath expression: " +
                                                                  xPathExpr, e);
          }
          return hostsPorts;
      }
  
      /**
       * Obtains the host:port pairs based on a role.
       * @param roleNode The node representing the role
       * @return The host:port pairs
       */
      public List<NameValuePair<Integer>> getHostPorts(Element roleNode) {
          ArrayList<NameValuePair<Integer>> hostsPorts = null;
          try {
              Node hostPortNode = (Node) xPath.evaluate(
                      "fa:hostConfig/fa:hostPorts", roleNode, XPathConstants.NODE);
              if (hostPortNode != null)
                  hostsPorts = hostPortsTable.get(hostPortNode);
          } catch (XPathExpressionException e) {
              logger.log(Level.WARNING, "Error processing XPath expression: " +
                      "fa:hostConfig/fa:hostPorts", e);
          }
          return hostsPorts;
      }
  
      /**
       * Returns NodeList for a given xpath string.
       * @param xPathExpr
       * @return NodeList
       */
      public NodeList getNodeList(String xPathExpr) {
          NodeList nodes = null;
          if(xPathExpr.charAt(0) != '/')
              xPathExpr = "//" + xPathExpr;
          else    //the JXPathContext expects 'params' which is the variable name returned by XMLFile
              xPathExpr = "params" + xPathExpr;
  
          try {
              nodes = (NodeList) xPath.evaluate(xPathExpr,
                                                  doc, XPathConstants.NODESET);
          } catch (XPathExpressionException e) {
              logger.log(Level.WARNING, "Error processing XPath expression: " +
                                                                  xPathExpr, e);
          }
          return nodes;
      }
  
      /**
       * Runs some testing of the XML reader.
       * @param args The command line arguments
       * @throws Exception If there are errors with the test
       */
      public static void main(String[] args) throws Exception {
          if(args.length < 3)
              System.out.println("Usage : java XMLReader <XML File> <XPath exp>");
  
          XMLReader util = new XMLReader(args[0], true, false);
          util.processHostPorts();
  
          System.out.println("File : " + args[0]);
          System.out.println("XPath : " + args[1]);
  
          System.out.print("\ngetValues : " + args[1] + " = ");
          String[] values = (String[])util.getValues(args[1]).toArray(new String[1]);
          for(int j = 0; j < values.length; j++)
              System.out.print(values[j] + "\t");
  
          System.out.print("\ngetValue : " + args[1] + " = ");
          System.out.println(util.getValue(args[1]));
  
          System.out.print("\ngetBooleanValue : " + args[1] + " = ");
          System.out.println(Boolean.toString(Boolean.valueOf(util.getValue(args[1])).booleanValue()));
  
      }
  }
  