/**
 * 
 */
package com.tandbergtv.watchpoint.pmm.util;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;

import javax.xml.XMLConstants;
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.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

/**
 * Provides utility methods for creating and manipulating DOM documents
 * 
 * @author Chandan Gudla
 * @author Sahil Verma
 */
public class XMLDocumentUtility {

	/* The Logger */
	private static final Logger logger = Logger.getLogger(XMLDocumentUtility.class);

	/**
	 * Sets the text content of a node specified by xPath in a given DOM node.
	 * 
	 * @param expression xPath expression used to locate the node
	 * @param value value to set for the node.
	 * @param xPath xPath evaluation object
	 * @param n DOM node being evaluated by xpath
	 * @throws Exception Exception in finding a matching node, or setting the text content.
	 */
	public static void setNodeValue(String expression, String value, XPath xPath, Node n)
			throws Exception {
		Node node = (Node) xPath.evaluate(expression, n, XPathConstants.NODE);
		node.setTextContent(value);
	}

	/**
	 * Gets the text content of a node specified by xPath expression in a given DOM node.
	 * 
	 * @param expression xPath expression used to locate the node
	 * @param xPath xPath evaluation object
	 * @param n DOM node being evaluated by xpath
	 * @return The text content of the matching node found, or empty string if no match found.
	 * @throws Exception Exception when evaluating the xpath expression and find the matching node
	 */
	public static String getNodeValue(String expression, XPath xPath, Node n) throws Exception {
		if (expression.substring(0, 1).equals("$"))
			return expression.substring(1);

		Node node = (Node) xPath.evaluate(expression, n, XPathConstants.NODE);
		if (node != null)
			return node.getTextContent();

		return "";
	}

	/**
	 * Gets the set of nodes specified by xPath expression in a given DOM document.
	 * 
	 * @param expression xPath expression used to locate the nodes
	 * @param xPath xPath evaluation object
	 * @param doc DOM document being evaluated by xpath
	 * @return The NodeList containing all matching nodes.
	 * @throws Exception Exception in evaluating the XPath expression
	 */
	public static NodeList getNodeSet(String expression, XPath xPath, Document doc)
			throws Exception {
		return (NodeList) xPath.evaluate(expression, doc, XPathConstants.NODESET);
	}

	/**
	 * Get the xml string from a given DOM document using the specified encoding
	 * 
	 * @param document the DOM Document from which the xml will be created
	 * @return The entire document serialized into a String
	 * @throws Exception Exception when attempting to serialize the document.
	 */
	public static String convertToString(Node node, String encoding) throws Exception {
		StringWriter w = new StringWriter();
		Transformer transformer = TransformerFactory.newInstance().newTransformer();

		transformer.setOutputProperty(OutputKeys.ENCODING, encoding);
		transformer.transform(new DOMSource(node), new StreamResult(w));

		return w.getBuffer().toString();
	}

	/**
	 * Get the xml string from a given DOM document. The string contains a byte sequence in the
	 * document's encoding.
	 * 
	 * @param document the DOM Document from which the xml will be created
	 * @return The entire document serialized into a String
	 * @throws Exception Exception when attempting to serialize the document.
	 */
	public static String convertToString(Node node) throws Exception {
		StringWriter w = new StringWriter();
		Transformer transformer = TransformerFactory.newInstance().newTransformer();
		Document document = (node instanceof Document) ? Document.class.cast(node) : node
				.getOwnerDocument();

		if (document.getXmlEncoding() != null)
			transformer.setOutputProperty(OutputKeys.ENCODING, document.getXmlEncoding());

		transformer.transform(new DOMSource(node), new StreamResult(w));

		return w.getBuffer().toString();
	}

	/**
	 * Creates and returns a DOM document from a string containing xml.
	 * 
	 * @param xmlString the string that contains xml
	 * @return The DOM document created from the XML String
	 * @throws Exception Exception attempting to build the document from the XML String.
	 */
	public static Document loadXml(String xmlString) throws Exception {
		return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(
				new InputSource(new StringReader(xmlString)));
	}

	/**
	 * Creates and returns a DOM document from a string containing xml.
	 * 
	 * @param xmlString the string that contains xml
	 * @return The DOM document created from the XML String
	 * @throws Exception Exception attempting to build the document from the XML String.
	 */
	public static Document loadXml(String xmlString, boolean isNSAware) throws Exception {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

		factory.setNamespaceAware(isNSAware);

		return factory.newDocumentBuilder().parse(new InputSource(new StringReader(xmlString)));
	}

	/**
	 * Gets a DOM Document from the file path specified. The path must resolve correctly to a
	 * resource.
	 * 
	 * @param classloader The classloader to use when loading the file containing the XML document.
	 * @param filePath the resource path of the xml file
	 * @param validating determines whether the document will be validated or not
	 * @return The DOM Document created from the contents of the specified file.
	 * @throws Exception Failure loading the contents of the file into a DOM document.
	 */
	public static Document loadFile(ClassLoader classloader, String filePath, boolean validating)
			throws Exception {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

		factory.setValidating(validating);

		InputStream stream = classloader.getResourceAsStream(filePath);

		return factory.newDocumentBuilder().parse(stream);
	}

	/**
	 * Gets a DOM Document from the file path specified without performing any validation. The path
	 * must resolve correctly to a resource.
	 * 
	 * @param filePath the path of the xml file
	 * @return The DOM Document created from the contents of the specified file.
	 * @throws Exception Failure loading the contents of the file into a DOM document.
	 */
	public static Document loadFile(String filePath) throws Exception {
		return XMLDocumentUtility.loadFile(XMLDocumentUtility.class.getClassLoader(), filePath);
	}

	/**
	 * Gets a DOM Document from the file path specified using the given classLoader without
	 * performing any validation. The path must resolve correctly to a resource.
	 * 
	 * @param classloader The classloader to use when loading the file containing the XML document.
	 * @param filePath the path of the xml file
	 * @return The DOM Document created from the contents of the specified file.
	 * @throws Exception Failure loading the contents of the file into a DOM document.
	 */
	public static Document loadFile(ClassLoader classloader, String filePath) throws Exception {
		return XMLDocumentUtility.loadFile(classloader, filePath, false);
	}

	/**
	 * Loads a schema from the specified resource path
	 * 
	 * @param path The resource path to the schema document
	 * @return The loaded Schema
	 * @throws Exception Exception creating the Schema from the file contents.
	 */
	public static Schema loadSchema(String path) throws Exception {
		InputStream stream = XMLDocumentUtility.class.getClassLoader().getResourceAsStream(path);
		return SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(
				new StreamSource(stream));
	}

	/**
	 * Saves the specified DOM document to the path
	 * 
	 * @param document The DOM Document to save into a file.
	 * @param filePath The path of the file to save the XML document to.
	 * @throws Exception Failure serializing the Document, or saving to a file.
	 */
	public static void save(Document document, String filePath) throws Exception {
		Transformer transformer = TransformerFactory.newInstance().newTransformer();

		/* This Property: indent-amount works only with the new version of XALAN */
		transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
		transformer.setOutputProperty(OutputKeys.INDENT, "yes");

		if (document.getXmlEncoding() != null)
			transformer.setOutputProperty(OutputKeys.ENCODING, document.getXmlEncoding());

		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(new File(filePath));
			transformer.transform(new DOMSource(document.getDocumentElement()), new StreamResult(
					fos));
		} finally {
			if (fos != null) {
				try {
					fos.close();
				} catch (Exception e) {
					logger.warn("Failed to close file: " + filePath, e);
				}
			}
		}
	}
}
