/**
* Copyright 2014
* SMEdit https://github.com/StarMade/SMEdit
* SMTools https://github.com/StarMade/SMTools
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
**/
package jo.sm.logic.utils;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
/**
* @author jgrant
*
*/
public class XMLUtils {
private static final Logger log = Logger.getLogger(XMLUtils.class.getPackage().getName());
private static DocumentBuilderFactory mFactory = null;
/**
* @return A new document to populate
*/
public static Document newDocument() {
ensureFactory();
try {
DocumentBuilder builder = mFactory.newDocumentBuilder();
return builder.newDocument();
} catch (ParserConfigurationException e) {
return null;
}
}
/**
* @param is the input stream to read
* @return the document contained in that input stream
*/
public static Document readStream(InputStream is) {
ensureFactory();
try {
DocumentBuilder builder = mFactory.newDocumentBuilder();
return builder.parse(is);
} catch (IOException | ParserConfigurationException | SAXException e) {
e.printStackTrace();
return null;
}
}
/**
* @param xmlFile is the file to read
* @return the Document contained in that file
*/
public static Document readFile(File xmlFile) {
return readFile(xmlFile, false);
}
/**
* @param xmlFile is the file to read
* @param compress if true, the file is assumed to be compressed with GZIP
* @return the Document contained in that file
*/
public static Document readFile(File xmlFile, boolean compress) {
ensureFactory();
try {
DocumentBuilder builder = mFactory.newDocumentBuilder();
//builder.setEntityResolver(new EntityUtils());
Document ret = null;
if (xmlFile.exists() && xmlFile.canRead()) {
InputStream is = new FileInputStream(xmlFile);
if (compress) {
is = new GZIPInputStream(is);
}
ret = builder.parse(is);
is.close();
} else {
log.log(Level.FINER, "XML file=''{0}'' does not exist or can not be read", xmlFile.getName());
}
return ret;
} catch (IOException | ParserConfigurationException | SAXException e) {
log.log(Level.SEVERE, "Cannot read file '" + xmlFile + ", compress=" + compress, e);
return null;
}
}
/**
* @param xmlURL is a URL to an XML document
* @return the Document contained in the content at tha tURL
*/
public static Document readURL(String xmlURL) {
ensureFactory();
try {
URL u = new URL(xmlURL);
DocumentBuilder builder = mFactory.newDocumentBuilder();
return builder.parse(u.openStream());
} catch (IOException | ParserConfigurationException | SAXException e) {
log.log(Level.WARNING, "readURL failed!", e);
e.printStackTrace();
return null;
}
}
/**
* @param xml is XML for a document
* @return the Document contained in that XML
*/
public static Document readString(String xml) {
ensureFactory();
try {
DocumentBuilder builder = mFactory.newDocumentBuilder();
return builder.parse(new ByteArrayInputStream(xml.getBytes("UTF-8")));
} catch (IOException | ParserConfigurationException | SAXException e) {
if (e instanceof SAXParseException) {
SAXParseException spe = (SAXParseException) e;
int l = spe.getLineNumber();
int c = spe.getColumnNumber();
StringTokenizer st = new StringTokenizer(xml, "\r\n");
String line = "";
for (int i = 0; (i < l) && st.hasMoreTokens(); i++) {
line = st.nextToken();
}
log.log(Level.INFO, "Line=" + l + ", column=" + c);
log.log(Level.INFO, line);
for (int i = 0; i < c - 1; i++) {
log.log(Level.INFO, "-");
}
log.log(Level.INFO, "^");
}
log.log(Level.INFO, "",e);
e.printStackTrace();
return null;
}
}
private static synchronized void ensureFactory() {
if (mFactory == null) {
mFactory = DocumentBuilderFactory.newInstance();
//mFactory.setIgnoringComments(true);
mFactory.setValidating(false);
mFactory.setCoalescing(true);
mFactory.setExpandEntityReferences(false);
}
}
/**
* @param root the root node to search underneath (non-inclusive)
* @param path a slash delimited list of hierarchal node names
* @param key an attribute value to test on qualifying nodes
* @param value the value expected in that attribute
* @return the list of nodes matching the criteria
*/
public static List<Node> findNodes(Node root, String path, String key, String value) {
Map<String, String> map = new HashMap<>();
map.put(key, value);
return findNodes(root, path, map);
}
/**
* @param root the root node to search underneath (non-inclusive)
* @param path a slash delimited list of hierarchal node names
* @param key an attribute value to test on qualifying nodes
* @param value the value expected in that attribute
* @return the first node matching the criteria
*/
public static Node findFirstNode(Node root, String path, String key, String value) {
Map<String, String> map = new HashMap<>();
map.put(key, value);
return findFirstNode(root, path, map);
}
/**
* @param root the root node to search underneath (non-inclusive)
* @param path a slash delimited list of hierarchal node names
* @param attributeMap a key-value map of attribute-value pairs to test for
* inclusion
* @return the list of nodes matching the criteria
*/
public static List<Node> findNodes(Node root, String path, Map<String, String> attributeMap) {
List<Node> nodes = findNodes(root, path);
for (Iterator<Node> i = nodes.iterator(); i.hasNext();) {
if (!attributeMatch(i.next(), attributeMap)) {
i.remove();
}
}
return nodes;
}
/**
* @param root the root node to search underneath (non-inclusive)
* @param path a slash delimited list of hierarchal node names
* @param attributeMap a key-value map of attribute-value pairs to test for
* inclusion
* @return the first node matching the criteria
*/
public static Node findFirstNode(Node root, String path, Map<String, String> attributeMap) {
Node node = findFirstNode(root, path);
if ((node != null) && !attributeMatch(node, attributeMap)) {
node = null;
}
return node;
}
/**
* @param n a Node to examine attributes on
* @param attributeMap a key-value map of attribute-value pairs to test
* @return true if all keys are present and contain the mapped values
*/
public static boolean attributeMatch(Node n, Map<String, String> attributeMap) {
for (String key : attributeMap.keySet()) {
String val = (String) attributeMap.get(key);
if (!val.equals(XMLUtils.getAttribute(n, key))) {
return false;
}
}
return true;
}
/**
* @param root the root node to search underneath (non-inclusive)
* @param path a slash delimited list of hierarchal node names
* @return the list of nodes matching the criteria
*/
public static List<Node> findNodes(Node root, String path) {
List<Node> ret = new ArrayList<>();
if (path != null) {
String[] paths = ArrayUtils.toStringArray(new StringTokenizer(path, "/,"));
findNodes(ret, root, paths, 0);
}
return ret;
}
/**
* @param root the root node to search underneath (non-inclusive)
* @param path a slash delimited list of hierarchal node names
* @return the first node matching the criteria
*/
public static Node findFirstNode(Node root, String path) {
List<Node> list = findNodes(root, path);
if (list.isEmpty()) {
return null;
}
return list.get(0);
}
/**
* @param found a list of Nodes to add found nodes to
* @param root the node from which to search (non-inclusive)
* @param path a list of node names to match
* @param o where in the list we are
*/
public static void findNodes(List<Node> found, Node root, String[] path, int o) {
for (Node n = root.getFirstChild(); n != null; n = n.getNextSibling()) {
if (n.getNodeName().startsWith("#")) {
continue;
}
if (path[o].equals("*")) {
if (o + 1 == path.length) {
found.add(n);
} else {
if (n.getNodeName().equals(path[o + 1])) {
if (o + 2 == path.length) {
found.add(n);
} else {
findNodes(found, n, path, o + 2);
}
} else {
findNodes(found, n, path, o);
}
}
} else {
String nn = n.getNodeName();
int off = nn.indexOf(':');
if (off >= 0) {
nn = nn.substring(off + 1);
}
if (nn.equals(path[o])) {
if (o + 1 == path.length) {
found.add(n);
} else {
findNodes(found, n, path, o + 1);
}
}
}
}
}
/**
* @param n a root node to search beneath
* @param name a node name to look for
* @return the first instance of that node name in the tree beneath the root
* (depth first)
*/
public static Node findFirstNodeRecursive(Node n, String name) {
List<Node> nodes = new ArrayList<>();
findRecursive(n, name, nodes, true);
if (nodes.isEmpty()) {
return null;
}
return nodes.get(0);
}
/**
* @param n a root node to search beneath
* @param name a node name to look for
* @return all instances of that node name in the tree beneath the root
*/
public static List<Node> findAllNodesRecursive(Node n, String name) {
List<Node> nodes = new ArrayList<>();
findRecursive(n, name, nodes, false);
return nodes;
}
private static void findRecursive(Node parent, String name, List<Node> nodes, boolean onlyOne) {
String nn = parent.getNodeName();
int off = nn.indexOf(':');
if (off >= 0) {
nn = nn.substring(off + 1);
}
if (nn.equals(name)) {
nodes.add(parent);
if (onlyOne) {
return;
}
}
for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) {
findRecursive(child, name, nodes, onlyOne);
if (onlyOne && (nodes.size() > 0)) {
return;
}
}
}
/**
* @param n Node to examine
* @param attr Attribute to look for
* @return true if the Node contains the named Attribute
*/
public static boolean hasAttribute(Node n, String attr) {
NamedNodeMap attrs = n.getAttributes();
if (attrs == null) {
return false;
}
Node ret = attrs.getNamedItem(attr);
return ret != null;
}
/**
* @param n Node to examine
* @param attr Attribute to look for
* @param def Default value to return if attribute is not present
* @return if the Node contains the named Attribute, the value, if not, the
* def parameter
*/
public static String getAttribute(Node n, String attr, String def) {
NamedNodeMap attrs = n.getAttributes();
if (attrs == null) {
return def;
}
Node ret = attrs.getNamedItem(attr);
if (ret == null) {
return def;
} else {
return ret.getNodeValue();
}
}
/**
* @param n Node to examine
* @param attr Attribute to look for
* @return if the Node contains the named Attribute, the value, if not,
* empty string
*/
public static String getAttribute(Node n, String attr) {
return getAttribute(n, attr, "");
}
/**
* @param n the node to look for text on
* @return The conjoined values of all #text nodes immediately under this
* node
*/
public static String getText(Node n) {
StringBuilder sb = new StringBuilder();
for (Node ele = n.getFirstChild(); ele != null; ele = ele.getNextSibling()) {
String name = ele.getNodeName();
if (name.equalsIgnoreCase("#text")) {
sb.append(ele.getNodeValue());
}
}
return sb.toString().trim();
}
/**
* @param n the Node to look for a text node under
* @param nodeName the name of the text node to look for
* @return finds the first node named nodeName and returns any text under it
*/
public static String getTextTag(Node n, String nodeName) {
Node textNode = findFirstNode(n, nodeName);
if (textNode == null) {
return "";
} else {
return getText(textNode);
}
}
/**
* @param n Node to look under
* @return true if the given node contains any #text children
*/
public static boolean isTextNode(Node n) {
for (Node ele = n.getFirstChild(); ele != null; ele = ele.getNextSibling()) {
String name = ele.getNodeName();
if (!name.equalsIgnoreCase("#text")) {
return false;
}
}
return true;
}
/**
* @param xml some raw XML
* @return formatted an indented XML text
*/
public static String format(String xml) {
return format(xml, false);
}
/**
* @param xml some raw XML
* @param latin1 true if latin1 entities are to be encoded
* @return formatted an indented XML text
*/
public static String format(String xml, boolean latin1) {
Document doc = readString(xml);
StringBuffer sb = new StringBuffer();
sb.append("<?xml version=\"");
//sb.append(doc.getXmlVersion());
sb.append("1.0");
sb.append("\" encoding=\"");
//sb.append(doc.getXmlEncoding());
sb.append("UTF-8");
sb.append("\"?>\n");
format(doc.getLastChild(), sb, "", latin1);
return sb.toString();
}
private static void format(Node n, StringBuffer out, String indent, boolean latin1) {
if ("#text".equals(n.getNodeName())) {
String txt = n.getNodeValue().trim();
if (txt.length() > 0) {
out.append(indent);
out.append(n.getNodeValue());
out.append("\n");
}
return;
}
if ("#comment".equals(n.getNodeName())) {
out.append("<!--");
out.append(n.getNodeValue());
out.append("-->\n");
return;
}
out.append(indent);
out.append("<");
out.append(n.getNodeName());
NamedNodeMap map = n.getAttributes();
if (map != null) {
if (map.getLength() == 1) {
Node ele = map.item(0);
String v = ele.getNodeValue();
if (v != null) {
out.append(" ");
out.append(ele.getNodeName());
out.append("=\"");
out.append(EntityUtils.insertEntities(v, latin1));
out.append("\"");
}
} else {
for (int i = 0; i < map.getLength(); i++) {
Node ele = map.item(i);
out.append("\n");
out.append(indent);
out.append(" ");
out.append(ele.getNodeName());
out.append("=\"");
out.append(EntityUtils.insertEntities(ele.getNodeValue(), latin1));
out.append("\"");
}
}
}
if (isTextNode(n)) {
String txt = getText(n);
if (txt.length() == 0) {
out.append("/>\n");
} else {
out.append(">");
out.append(EntityUtils.insertEntities(txt, latin1));
out.append("</");
out.append(n.getNodeName());
out.append(">\n");
}
} else {
out.append(">\n");
for (Node ele = n.getFirstChild(); ele != null; ele = ele.getNextSibling()) {
format(ele, out, indent + " ", latin1);
}
out.append(indent);
out.append("</");
out.append(n.getNodeName());
out.append(">\n");
}
}
/**
* @param node root of tree to write
* @param f file to write to
* @return true if successful
*/
public static boolean writeFile(Node node, File f) {
return writeFile(node, f, false);
}
/**
* @param node root of tree to write
* @param f file to write to
* @param latin1 true if latin1 entities are to be encoded
* @return true if successful
*/
public static boolean writeFile(Node node, File f, boolean latin1) {
return writeFile(node, f, latin1, false);
}
/**
* @param node root of tree to write
* @param f file to write to
* @param latin1 true if latin1 entities are to be encoded
* @param compress true if GZIP compression to be used on file
* @return true if successful
*/
public static boolean writeFile(Node node, File f, boolean latin1, boolean compress) {
if (node instanceof Document) {
node = node.getFirstChild();
}
try {
StringBuffer sb = new StringBuffer();
format(node, sb, "", latin1);
OutputStream os = new FileOutputStream(f);
if (compress) {
os = new GZIPOutputStream(os);
}
try (OutputStreamWriter fw = new OutputStreamWriter(os)) {
fw.write(sb.toString());
}
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* @param node Node to start from
* @return the XML contents in that node in a string
*/
public static String writeString(Node node) {
return writeString(node, false);
}
/**
* @param node Node to start from
* @param latin1 true if latin1 entities are to be encoded
* @return the XML contents in that node in a string
*/
public static String writeString(Node node, boolean latin1) {
StringBuffer sb = new StringBuffer();
format(node, sb, "", latin1);
return sb.toString();
}
/**
* @param n1 first Node to test
* @param n2 second Node to test
* @return true if a deep compare show the same children and attributes in
* the same order
*/
public static boolean equals(Node n1, Node n2) {
// compare type
if (!n1.getNodeName().equals(n2.getNodeName())) {
return false;
}
// compare attributes
NamedNodeMap nnm1 = n1.getAttributes();
NamedNodeMap nnm2 = n2.getAttributes();
if (nnm1.getLength() != nnm2.getLength()) {
return false;
}
for (int i = 0; i < nnm1.getLength(); i++) {
Node attr1 = nnm1.item(i);
if (!getAttribute(n1, attr1.getNodeName()).equals(getAttribute(n2, attr1.getNodeName()))) {
return false;
}
}
// compare children
Node c1 = n1.getFirstChild();
Node c2 = n2.getFirstChild();
for (;;) {
while ((c1 != null) && c1.getNodeName().startsWith("#")) {
c1 = c1.getNextSibling();
}
while ((c2 != null) && c2.getNodeName().startsWith("#")) {
c2 = c2.getNextSibling();
}
if ((c1 == null) && (c2 == null)) {
break;
}
if ((c1 == null) || (c2 == null)) {
return false;
}
if (!equals(c1, c2)) {
return false;
}
c1 = c1.getNextSibling();
c2 = c2.getNextSibling();
}
return true;
}
}