/*
* Copyright (c) 2012-2015 iWave Software LLC
* All Rights Reserved
*/
package com.iwave.ext.xml;
import java.io.ByteArrayInputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
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.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.text.StrBuilder;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
public class XmlUtils {
/** Reusable document builder factory. */
private static DocumentBuilderFactory documentBuilderFactory;
/** Reusable transformer factory. */
private static TransformerFactory transformerFactory;
/** Reusable XPath factory. */
private static XPathFactory xpathFactory;
/** Reusable XPath. */
private static XPath xpath;
/**
* Gets the document builder factory, creating it if necessary.
*
* @return the document builder factory.
*/
public static synchronized DocumentBuilderFactory getDocumentBuilderFactory() {
if (documentBuilderFactory == null) {
documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setNamespaceAware(true);
documentBuilderFactory.setExpandEntityReferences(false);
}
return documentBuilderFactory;
}
/**
* Gets the transformer factory, creating it if necessary.
*
* @return the transformer factory.
*/
public static synchronized TransformerFactory getTransformerFactory() {
if (transformerFactory == null) {
transformerFactory = TransformerFactory.newInstance();
}
return transformerFactory;
}
/**
* Gets the XPath factory, creating it if necessary.
*
* @return the XPath factory.
*/
public static synchronized XPathFactory getXPathFactory() {
if (xpathFactory == null) {
xpathFactory = XPathFactory.newInstance();
}
return xpathFactory;
}
/**
* Gets the XPath API, creating it if necessary.
*
* @return the XPath API.
*/
public static synchronized XPath getXPath() {
if (xpath == null) {
xpath = getXPathFactory().newXPath();
}
return xpath;
}
/**
* Creates an XPath instance with the given namespace mapping.
*
* @param namespaces the prefix->namespaceURI mapping.
* @return the XPath instance.
*/
public static XPath createXPath(Map<String, String> namespaces) {
XPath xpath = getXPathFactory().newXPath();
xpath.setNamespaceContext(new NamespaceContextMap(namespaces));
return xpath;
}
/**
* Creates an XPath instance with the given namespace mapping. The namespace strings take the
* form <code><i>prefix</i>=<i>namespaceURI</i></code>.
*
* @param namespaces the namespaces.
* @return the XPath instance.
*/
public static XPath createXPath(String... namespaces) {
Map<String, String> namespaceMap = new HashMap<String, String>();
for (String namespace : namespaces) {
String prefix = StringUtils.substringBefore(namespace, "=");
String namespaceURI = StringUtils.substringAfter(namespace, "=");
namespaceMap.put(prefix, namespaceURI);
}
return createXPath(namespaceMap);
}
/**
* Parses the XML string into a Document.
*
* @param xml the XML string.
* @return the XML Document.
*/
public static Document parseXml(String xml) {
try {
ByteArrayInputStream xmlStream = new ByteArrayInputStream(xml.getBytes("UTF-8"));
return getDocumentBuilderFactory().newDocumentBuilder().parse(xmlStream);
} catch (UnsupportedEncodingException e) {
throw new Error("UTF-8 must be supported");
} catch (Exception e) {
throw new XmlException(e);
}
}
/**
* Formats the XML as a string.
*
* @param node the XML node (typically a Document or Element).
* @return the formatted XML string.
*/
public static String formatXml(Node node) {
return formatXml(node, true);
}
/**
* Formats the XML as a string.
*
* @param node the XML node (typically a Document or Element).
* @param pretty whether to make the XML pretty.
* @return the formatted XML string.
*/
public static String formatXml(Node node, boolean pretty) {
try {
StringWriter writer = new StringWriter();
Source source = new DOMSource(node);
Result result = new StreamResult(writer);
Transformer t = getTransformerFactory().newTransformer();
t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
if (pretty) {
t.setOutputProperty(OutputKeys.INDENT, "yes");
t.setOutputProperty("{http://xml.apache.org/xalan}indent-amount", "2");
}
t.transform(source, result);
return writer.toString();
} catch (Exception e) {
throw new XmlException(e);
}
}
/**
* Escapes any XML entities in the text.
*
* @param text the text.
* @return the escaped text.
*/
public static String escapeText(String text) {
StrBuilder sb = new StrBuilder();
escape(sb, text, false, false);
return sb.toString();
}
/**
* Escapes any XML entities in the text for use in an attribute.
*
* @param text the text.
* @return the escaped text.
*/
public static String escapeAttr(String text) {
StrBuilder sb = new StrBuilder();
escape(sb, text, true, true);
return sb.toString();
}
/**
* Escapes any XML entities in the text, appending the result to the StrBuilder.
*
* @param toAppend the StrBuilder to append the escaped text to.
* @param text the text to escape.
* @param escapeQuote whether quote characters will be escaped.
* @param escapeApos whether apostrophe characters will be escaped.
*/
public static void escape(StrBuilder toAppend, String text, boolean escapeQuote,
boolean escapeApos) {
if (text == null) {
return;
}
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
switch (ch) {
case '<':
toAppend.append("<");
break;
case '>':
toAppend.append(">");
break;
case '&':
toAppend.append("&");
break;
case '"':
toAppend.append(escapeQuote ? """ : ch);
break;
case '\'':
toAppend.append(escapeApos ? "'" : ch);
break;
default:
toAppend.append(ch);
break;
}
}
}
/**
* Creates a stylesheet template from the given source.
*
* @param source the stylesheet source.
* @return the compiled template of the stylesheet.
*/
public static Templates createTemplates(Source source) {
try {
return getTransformerFactory().newTemplates(source);
} catch (TransformerConfigurationException e) {
throw new XmlException(e);
}
}
/**
* Creates an identity transformer.
*
* @return the identity transformer.
*/
public static Transformer createTransformer() {
try {
return getTransformerFactory().newTransformer();
} catch (TransformerConfigurationException e) {
throw new XmlException(e);
}
}
/**
* Creates a transformer from the given stylesheet source.
*
* @param source the stylesheet source.
* @return the transformer.
*/
public static Transformer createTransformer(Source source) {
try {
return getTransformerFactory().newTransformer(source);
} catch (TransformerConfigurationException e) {
throw new XmlException(e);
}
}
/**
* Compiles an XPath expresssion.
*
* @param xpath the XPath api.
* @param expr the expression.
* @return the compiled expression.
*/
public static XPathExpression compileXPath(XPath xpath, String expr) {
try {
return xpath.compile(expr);
} catch (XPathExpressionException e) {
throw new XmlException(e);
}
}
/**
* Compiles an XPath expression using the default XPath.
*
* @param expr the expression.
* @return the compiled expression.
*/
public static XPathExpression compileXPath(String expr) {
return compileXPath(getXPath(), expr);
}
/**
* Compiles an XPath expression, which is aware of the specified namespaces. This is a
* convenience method for creating single XPath expressions. If multiple expressions are to be
* compiled for the same set of namespaces, create an XPath using {@link #createXPath(String...)} and compile each expression using
* {@link #compileXPath(XPath, String)}.
*
* @param expr the XPath expression.
* @param namespaces the namespaces (<code><i>prefix</i>=<i>namespaceURI</i></code> format).
* @return the compiled expression.
*
* @see #createXPath(String...)
* @see #compileXPath(XPath, String)
*/
public static XPathExpression compileXPath(String expr, String... namespaces) {
return compileXPath(createXPath(namespaces), expr);
}
/**
* Evaluates the XPath expression as text.
*
* @param expr the expression.
* @param context the context node.
* @return the text.
*/
public static String selectText(XPathExpression expr, Node context) {
try {
return (String) expr.evaluate(context, XPathConstants.STRING);
} catch (XPathExpressionException e) {
throw new XmlException(e);
}
}
/**
* Evaluates the XPath expression as an Integer.
*
* @param expr
* the expression.
* @param context
* the context node.
* @return the integer, or null if the value is not an integer.
*/
public static Integer selectInteger(XPathExpression expr, Node context) {
String value = selectText(expr, context);
if (StringUtils.isNotBlank(value)) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
return null;
}
}
else {
return null;
}
}
/**
* Evaluates the XPath expression as a Long.
*
* @param expr
* the expression.
* @param context
* the context node.
* @return the long, or null if the value is not a long.
*/
public static Long selectLong(XPathExpression expr, Node context) {
String value = selectText(expr, context);
if (StringUtils.isNotBlank(value)) {
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
return null;
}
}
else {
return null;
}
}
/**
* Evaluates the XPath expression to a single element.
*
* @param expr the expression to evaluate.
* @param context the context node.
* @return the single element (or null).
*/
public static Element selectElement(XPathExpression expr, Node context) {
try {
Object result = expr.evaluate(context, XPathConstants.NODE);
if (result == null) {
return null;
}
else if (result instanceof Element) {
return (Element) result;
}
else {
throw new XmlException("Not an element: " + result);
}
} catch (XPathExpressionException e) {
throw new XmlException(e);
}
}
/**
* Evaluates the XPath expression to a list of elements.
*
* @param expr the expression to evaluate.
* @param context the context node.
* @return the list of elements.
*/
public static List<Element> selectElements(XPathExpression expr, Node context) {
try {
List<Element> elements = new ArrayList<Element>();
NodeList result = (NodeList) expr.evaluate(context, XPathConstants.NODESET);
for (int i = 0; i < result.getLength(); i++) {
Node item = result.item(i);
if (item instanceof Element) {
elements.add((Element) item);
}
else {
throw new XmlException("Not an element: " + item);
}
}
return elements;
} catch (XPathExpressionException e) {
throw new XmlException(e);
}
}
/**
* Gets the text value of the given element.
*
* @param e the element.
* @return the text contents of the given element.s
*/
public static String getText(Element e) {
StringBuilder sb = null;
if (e != null) {
NodeList children = e.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
switch (node.getNodeType()) {
case Node.TEXT_NODE:
case Node.CDATA_SECTION_NODE:
if (sb == null) {
sb = new StringBuilder();
}
sb.append(node.getNodeValue());
break;
}
}
}
return (sb != null) ? sb.toString() : null;
}
/**
* Gets the first child element.
*
* @param e the element.
* @return the first child element.
*/
public static Element getFirstChildElement(Element e) {
if (e != null) {
NodeList children = e.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
return (Element) node;
}
}
}
return null;
}
/**
* This is an implementation of NamespaceContext that uses a Map to hold the
* prefix->namespaceURI mapping.
*
* @author jonnymiller
*/
public static class NamespaceContextMap implements NamespaceContext {
private Map<String, String> namespaces;
public NamespaceContextMap(Map<String, String> namespaces) {
this.namespaces = namespaces;
}
@Override
public String getNamespaceURI(String prefix) {
String namespaceURI = namespaces.get(prefix);
if (namespaceURI != null) {
return namespaceURI;
}
if (XMLConstants.XML_NS_PREFIX.equals(prefix)) {
return XMLConstants.XML_NS_URI;
}
if (XMLConstants.XMLNS_ATTRIBUTE.equals(prefix)) {
return XMLConstants.XMLNS_ATTRIBUTE_NS_URI;
}
return XMLConstants.NULL_NS_URI;
}
@Override
public String getPrefix(String namespaceURI) {
if (XMLConstants.XML_NS_URI.equals(namespaceURI)) {
return XMLConstants.XML_NS_PREFIX;
}
if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(namespaceURI)) {
return XMLConstants.XMLNS_ATTRIBUTE;
}
for (String prefix : namespaces.keySet()) {
String ns = namespaces.get(prefix);
if (namespaceURI.equals(ns)) {
return prefix;
}
}
return null;
}
@Override
@SuppressWarnings("rawtypes")
public Iterator getPrefixes(String namespaceURI) {
if (XMLConstants.XML_NS_URI.equals(namespaceURI)) {
return Collections.singletonList(XMLConstants.XML_NS_PREFIX).iterator();
}
if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(namespaceURI)) {
return Collections.singletonList(XMLConstants.XMLNS_ATTRIBUTE).iterator();
}
List<String> prefixes = new ArrayList<String>();
for (String prefix : namespaces.keySet()) {
String ns = namespaces.get(prefix);
if (namespaceURI.equals(ns)) {
prefixes.add(prefix);
}
}
return Collections.unmodifiableList(prefixes).iterator();
}
}
}