/*
* The MIT License (MIT)
*
* Copyright (c) 2016 Lachlan Dowding
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package permafrost.tundra.xml.dom;
import com.wm.app.b2b.server.ServiceException;
import com.wm.data.IData;
import com.wm.data.IDataCursor;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import permafrost.tundra.data.IDataMap;
import permafrost.tundra.io.InputStreamHelper;
import permafrost.tundra.lang.CharsetHelper;
import permafrost.tundra.lang.ExceptionHelper;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
/**
* A collection of convenience methods for working with org.w3c.dom.Node objects.
*/
public final class NodeHelper {
/**
* Disallow instantiation of this class.
*/
private NodeHelper() {}
/**
* Serializes a Node object to a stream using the default character set.
*
* @param node The Node to be serialized.
* @return The serialized Node.
* @throws ServiceException If an XML transformation error occurs.
*/
public static InputStream emit(Node node) throws ServiceException {
return emit(node, null);
}
/**
* Serializes a Node object to a stream using the given character set.
*
* @param node The Node to be serialized.
* @param charset The character set to use.
* @return The serialized Node.
* @throws ServiceException If an XML transformation error occurs.
*/
public static InputStream emit(Node node, Charset charset) throws ServiceException {
if (node == null) return null;
InputStream content = null;
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
TransformerFactory transformerFactory = TransformerFactory.newInstance();
try {
// defend against denial of service attacks
transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
} catch(Throwable ex) {
// do nothing, method is not supported
}
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.ENCODING, CharsetHelper.normalize(charset).displayName());
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, node instanceof Document ? "no" : "yes");
transformer.transform(new DOMSource(node), new StreamResult(byteArrayOutputStream));
content = InputStreamHelper.normalize(byteArrayOutputStream.toByteArray());
} catch (TransformerException ex) {
ExceptionHelper.raise(ex);
}
return content;
}
/**
* Returns an IData representation of the given Node.
*
* @param node The Node to be parsed.
* @return An IData[] representation of this object.
*/
public static IData parse(Node node) {
return parse(node, null);
}
/**
* Returns an IData representation of the given Node.
*
* @param node The Node to be parsed.
* @param namespaceContext Any namespace declarations used in the XML content.
* @return An IData[] representation of this object.
*/
public static IData parse(Node node, NamespaceContext namespaceContext) {
return parse(node, namespaceContext, true);
}
/**
* Returns an IData representation of the given Node.
*
* @param node The Node to be parsed.
* @param namespaceContext Any namespace declarations used in the XML content.
* @param recurse If true, child elements will be recursed and returned also.
* @return An IData[] representation of this object.
*/
public static IData parse(Node node, NamespaceContext namespaceContext, boolean recurse) {
IDataMap map = new IDataMap();
parse(node, namespaceContext, map, recurse);
return map;
}
/**
* Creates an IData representation of the given Node in the given IDataMap.
*
* @param node The Node to be parsed.
* @param namespaceContext Any namespace declarations used in the XML content.
* @param output The IDataMap in which the IData representation is created.
* @param recurse If true, child elements will be recursed and returned also.
*/
private static void parse(Node node, NamespaceContext namespaceContext, IDataMap output, boolean recurse) {
if (node == null || output == null) return;
if (node instanceof Document) {
Document document = (Document)node;
Node root = document.getDocumentElement();
IDataMap child = new IDataMap();
parse(root, namespaceContext, child, recurse);
output.put(getNodeName(root, namespaceContext), child, false);
} else if (node instanceof Element) {
Element element = (Element)node;
boolean hasChildElements = ElementHelper.hasChildElements(element);
boolean hasAttributes = element.hasAttributes();
String content = ElementHelper.getTextContent(element);
if (hasAttributes || hasChildElements) {
if (element.hasAttributes()) {
NamedNodeMap attributes = element.getAttributes();
int length = attributes.getLength();
for (int i = 0; i < length; i++) {
parse(attributes.item(i), namespaceContext, output, recurse);
}
}
output.put("*body", content, false);
if (recurse && hasChildElements) {
IDataCursor cursor = output.getCursor();
Nodes children = Nodes.of(element.getChildNodes());
for (Node childNode : children) {
if (childNode.getNodeType() == Node.ELEMENT_NODE) {
Element childElement = (Element)childNode;
String name = getNodeName(childElement, namespaceContext);
if (childElement.hasAttributes() || ElementHelper.hasChildElements(childElement)) {
IDataMap childNodeMap = new IDataMap();
parse(childElement, namespaceContext, childNodeMap, recurse);
cursor.insertAfter(name, childNodeMap);
} else {
cursor.insertAfter(name, ElementHelper.getTextContent(childElement));
}
}
}
cursor.destroy();
}
} else {
output.put("*body", content, false);
}
} else if (node instanceof Attr) {
Attr attribute = (Attr)node;
output.put("@" + getNodeName(attribute, namespaceContext), attribute.getValue(), false);
} else {
// do nothing for other node types
}
}
/**
* Returns the node name using the prefixes defined in the given namespace context rather than
* the prefixes used in the parsed XML.
*
* @param node The node to return the name of.
* @param namespaceContext The namespace context to use for prefixing qualified names.
* @return The name of the node.
*/
private static String getNodeName(Node node, NamespaceContext namespaceContext) {
if (node == null) return null;
String name = node.getNodeName();
if (name.startsWith("xmlns:")) {
String uri = node.getNodeValue();
if (uri != null && namespaceContext != null) {
String prefix = namespaceContext.getPrefix(uri);
if (prefix != null) {
if (!prefix.equals(XMLConstants.DEFAULT_NS_PREFIX)) {
// correct the prefix on the namespace declaration, if using a different one when parsing
name = "xmlns:" + prefix;
}
}
}
} else {
String uri = node.getNamespaceURI();
if (uri != null && namespaceContext != null) {
String prefix = namespaceContext.getPrefix(uri);
if (prefix != null) {
name = node.getLocalName();
if (!prefix.equals(XMLConstants.DEFAULT_NS_PREFIX)) {
name = prefix + ":" + name;
}
}
}
}
return name;
}
/**
* Returns an IData representation of the given Node object.
*
* @param node A Node object.
* @param namespaceContext The namespace context to use for prefixing qualified names.
* @return An IData representation of the given Node object.
* @throws ServiceException If an error occurs.
*/
public static IData reflect(Node node, NamespaceContext namespaceContext) throws ServiceException {
return reflect(node, namespaceContext, false);
}
/**
* Returns an IData representation of the given Node object.
*
* @param node A Node object.
* @param namespaceContext The namespace context to use for prefixing qualified names.
* @param recurse If true, child nodes will be recursed and returned also.
* @return An IData representation of the given Node object.
* @throws ServiceException If an error occurs.
*/
public static IData reflect(Node node, NamespaceContext namespaceContext, boolean recurse) throws ServiceException {
if (node == null) return null;
// if node is a Document, then use its root node instead
if (node instanceof Document) node = ((Document)node).getDocumentElement();
IDataMap map = new IDataMap();
map.put("node", node);
map.put("name.qualified", getNodeName(node, namespaceContext));
String localName = node.getLocalName();
if (localName != null) map.put("name.local", localName);
String namespaceURI = node.getNamespaceURI();
if (namespaceURI != null) {
String prefix = node.getPrefix();
if (prefix != null) {
if (namespaceContext != null) prefix = namespaceContext.getPrefix(namespaceURI);
map.put("name.prefix", prefix);
}
map.put("name.uri", namespaceURI);
}
map.put("type", nodeTypeToString(node.getNodeType()));
String value = getValue(node);
if (value != null) map.put("value", value);
if (recurse && node.hasAttributes()) map.put("attributes", reflect(node.getAttributes(), namespaceContext, recurse));
if (recurse && node.hasChildNodes()) {
boolean hasElementChildren = false;
Nodes children = Nodes.of(node.getChildNodes());
List<IData> list = new ArrayList<IData>(children.size());
for (Node child : children) {
if (child.getNodeType() == Node.ELEMENT_NODE) {
hasElementChildren = true;
list.add(reflect(child, namespaceContext, recurse));
}
}
if (hasElementChildren) map.put("elements", list.toArray(new IData[list.size()]));
}
return map;
}
/**
* Returns the value of the given node. If the node is an element, the value returned is the
* concatenated text from its text and CDATA child nodes.
*
* @param node A node to return the value of.
* @return The value of the given node.
*/
public static String getValue(Node node) {
String value;
if (node instanceof Element) {
value = ElementHelper.getTextContent((Element) node);
} else {
value = node.getNodeValue();
}
return value;
}
/**
* Returns a String representation of the given Node type.
*
* @param nodeType A Node type.
* @return A String representation of the given Node type.
*/
private static String nodeTypeToString(int nodeType) {
String output = null;
switch(nodeType) {
case Node.ATTRIBUTE_NODE:
output = "ATTRIBUTE_NODE";
break;
case Node.CDATA_SECTION_NODE:
output = "CDATA_SECTION_NODE";
break;
case Node.COMMENT_NODE:
output = "COMMENT_NODE";
break;
case Node.DOCUMENT_FRAGMENT_NODE:
output = "DOCUMENT_FRAGMENT_NODE";
break;
case Node.DOCUMENT_NODE:
output = "DOCUMENT_NODE";
break;
case Node.DOCUMENT_TYPE_NODE:
output = "DOCUMENT_TYPE_NODE";
break;
case Node.ELEMENT_NODE:
output = "ELEMENT_NODE";
break;
case Node.ENTITY_NODE:
output = "ENTITY_NODE";
break;
case Node.ENTITY_REFERENCE_NODE:
output = "ENTITY_REFERENCE_NODE";
break;
case Node.NOTATION_NODE:
output = "NOTATION_NODE";
break;
case Node.PROCESSING_INSTRUCTION_NODE:
output = "PROCESSING_INSTRUCTION_NODE";
break;
case Node.TEXT_NODE:
output = "TEXT_NODE";
break;
default:
throw new IllegalStateException("Unknown org.w3c.dom.Node type specified: " + nodeType);
}
return output;
}
/**
* Returns an IData[] representation of the given NamedNodeMap.
*
* @param namedNodeMap A NamedNodeMap object.
* @param namespaceContext The namespace context to use for prefixing qualified names.
* @param recurse If true, child nodes will be recursed and returned also.
* @return An IData[] representation of the given NamedNodeMap object.
* @throws ServiceException If an error occurs.
*/
public static IData[] reflect(NamedNodeMap namedNodeMap, NamespaceContext namespaceContext, boolean recurse) throws ServiceException {
if (namedNodeMap == null) return null;
int length = namedNodeMap.getLength();
IData[] output = new IData[length];
for (int i = 0; i < length; i++) {
output[i] = reflect(namedNodeMap.item(i), namespaceContext, recurse);
}
return output;
}
/**
* Returns an IData[] representation of the given NodeList object.
*
* @param nodeList A NodeList object.
* @param namespaceContext The namespace context to use for prefixing qualified names.
* @param recurse If true, child nodes will be recursed and returned also.
* @return An IData[] representation of the given NodeList object.
* @throws ServiceException If an error occurs.
*/
public static IData[] reflect(NodeList nodeList, NamespaceContext namespaceContext, boolean recurse) throws ServiceException {
if (nodeList == null) return null;
int length = nodeList.getLength();
IData[] output = new IData[length];
for (int i = 0; i < length; i++) {
output[i] = reflect(nodeList.item(i), namespaceContext, recurse);
}
return output;
}
}