package org.extremecomponents.util; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; 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; 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.w3c.dom.DOMConfiguration; import org.w3c.dom.DOMImplementation; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.bootstrap.DOMImplementationRegistry; import org.w3c.dom.ls.DOMImplementationLS; import org.w3c.dom.ls.LSException; import org.w3c.dom.ls.LSOutput; import org.w3c.dom.ls.LSSerializer; /** * Utilities related to DOM and XML usage. * * @author Stefan Schmidt * @author Ben Alex * @author Alan Stewart * @since 1.0 */ public final class XmlUtils { private static final Map<String, XPathExpression> compiledExpressionCache = new HashMap<String, XPathExpression>(); private static final XPath xpath = XPathFactory.newInstance().newXPath(); private static final TransformerFactory transformerFactory = TransformerFactory.newInstance(); private static final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); private XmlUtils() { } /** * Read an XML document from the supplied input stream and return a document. * * @param inputStream the input stream to read from (required). The stream is closed upon completion. * @return a document. */ public static final Document readXml(InputStream inputStream) { Assert.notNull(inputStream, "InputStream required"); try { return factory.newDocumentBuilder().parse(inputStream); } catch (Exception e) { throw new IllegalStateException("Could not open input stream", e); } finally { try { inputStream.close(); } catch (IOException inored) {} } } /** * Write an XML document to the OutputStream provided. This will use the pre-configured Roo provided Transformer. * * @param outputStream the output stream to write to. The stream is closed upon completion. * @param document the document to write. */ public static final void writeXml(OutputStream outputStream, Document document) { writeXml(createIndentingTransformer(), outputStream, document); } /** * Write an XML document to the OutputStream provided. This will use the provided Transformer. * * @param transformer the transformer (can be obtained from XmlUtils.createIndentingTransformer()) * @param outputStream the output stream to write to. The stream is closed upon completion. * @param document the document to write. */ public static final void writeXml(Transformer transformer, OutputStream outputStream, Document document) { Assert.notNull(transformer, "Transformer required"); Assert.notNull(outputStream, "OutputStream required"); Assert.notNull(document, "Document required"); transformer.setOutputProperty(OutputKeys.METHOD, "xml"); try { StreamResult streamResult = createUnixStreamResultForEntry(outputStream); transformer.transform(new DOMSource(document), streamResult); } catch (Exception e) { throw new IllegalStateException(e); } finally { try { outputStream.close(); } catch (IOException ignored) { // Do nothing } } } /** * Write an XML document to the OutputStream provided. This method will detect if the JDK supports the * DOM Level 3 "format-pretty-print" configuration and make use of it. If not found it will fall back to * using formatting offered by TrAX. * * @param outputStream the output stream to write to. The stream is closed upon completion. * @param document the document to write. */ public static void writeFormattedXml(OutputStream outputStream, Document document) { // Note that the "format-pretty-print" DOM configuration parameter can only be set in JDK 1.6+. DOMImplementation domImplementation = document.getImplementation(); if (domImplementation.hasFeature("LS", "3.0") && domImplementation.hasFeature("Core", "2.0")) { DOMImplementationLS domImplementationLS = null; try { domImplementationLS = (DOMImplementationLS) domImplementation.getFeature("LS", "3.0"); } catch (NoSuchMethodError nsme) { // Fall back to default LS DOMImplementationRegistry registry = null; try { registry = DOMImplementationRegistry.newInstance(); } catch (Exception e) { // DOMImplementationRegistry not available. Falling back to TrAX. writeXml(outputStream, document); return; } if (registry != null) { domImplementationLS = (DOMImplementationLS) registry.getDOMImplementation("LS"); } else { // DOMImplementationRegistry not available. Falling back to TrAX. writeXml(outputStream, document); } } if (domImplementationLS != null) { LSSerializer lsSerializer = domImplementationLS.createLSSerializer(); DOMConfiguration domConfiguration = lsSerializer.getDomConfig(); if (domConfiguration.canSetParameter("format-pretty-print", Boolean.TRUE)) { lsSerializer.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE); LSOutput lsOutput = domImplementationLS.createLSOutput(); lsOutput.setEncoding("UTF-8"); lsOutput.setByteStream(outputStream); try { lsSerializer.write(document, lsOutput); } catch (LSException lse) { throw new IllegalStateException(lse); } finally { try { outputStream.close(); } catch (IOException ignored) { // Do nothing } } } else { // DOMConfiguration 'format-pretty-print' parameter not available. Falling back to TrAX. writeXml(outputStream, document); } } else { // DOMImplementationLS not available. Falling back to TrAX. writeXml(outputStream, document); } } else { // DOM 3.0 LS and/or DOM 2.0 Core not supported. Falling back to TrAX. writeXml(outputStream, document); } } /** * Compares two DOM {@link Node nodes} by comparing the representations of the nodes as XML strings * * @param node1 the first node * @param node2 the second node * @return true if the XML representation node1 is the same as the XML representation of node2, otherwise false */ public static boolean compareNodes(Node node1, Node node2) { Assert.notNull(node1, "First node required"); Assert.notNull(node2, "Second node required"); // The documents need to be cloned as normalization has side-effects node1 = node1.cloneNode(true); node2 = node2.cloneNode(true); // The documents need to be normalized before comparison takes place to remove any formatting that interfere with comparison if (node1 instanceof Document && node2 instanceof Document) { ((Document) node1).normalizeDocument(); ((Document) node2).normalizeDocument(); } else { node1.normalize(); node2.normalize(); } return nodeToString(node1).equals(nodeToString(node2)); } /** * Converts a {@link Node node} to an XML string * * @param node the first element * @return the XML String representation of the node, never null */ public static String nodeToString(Node node) { try { StringWriter writer = new StringWriter(); createIndentingTransformer().transform(new DOMSource(node), new StreamResult(writer)); return writer.toString(); } catch (TransformerException e) { throw new IllegalStateException(e); } } /** * Creates a {@link StreamResult} by wrapping the given outputStream in an * {@link OutputStreamWriter} that transforms Windows line endings (\r\n) * into Unix line endings (\n) on Windows for consistency with Roo's templates. * * @param outputStream * @return StreamResult * @throws UnsupportedEncodingException */ private static StreamResult createUnixStreamResultForEntry(OutputStream outputStream) throws UnsupportedEncodingException { final Writer writer; if (System.getProperty("line.separator").equals("\r\n")) { writer = new OutputStreamWriter(outputStream, "ISO-8859-1") { public void write(char[] cbuf, int off, int len) throws IOException { for (int i = off; i < off + len; i++) { if (cbuf[i] != '\r' || (i < cbuf.length - 1 && cbuf[i + 1] != '\n')) { super.write(cbuf[i]); } } } public void write(int c) throws IOException { if (c != '\r') super.write(c); } public void write(String str, int off, int len) throws IOException { String orig = str.substring(off, off + len); String filtered = orig.replace("\r\n", "\n"); int lengthDiff = orig.length() - filtered.length(); if (filtered.endsWith("\r")) { super.write(filtered.substring(0, filtered.length() - 1), 0, len - lengthDiff - 1); } else { super.write(filtered, 0, len - lengthDiff); } } }; } else { writer = new OutputStreamWriter(outputStream, "ISO-8859-1"); } return new StreamResult(writer); } /** * Checks in under a given root element whether it can find a child element * which matches the XPath expression supplied. Returns {@link Element} if * exists. * * Please note that the XPath parser used is NOT namespace aware. So if you * want to find a element <beans><sec:http> you need to use the following * XPath expression '/beans/http'. * * @param xPathExpression the xPathExpression (required) * @param root the parent DOM element (required) * @return the Element if discovered (null if not found) */ public static Element findFirstElement(String xPathExpression, Element root) { Node node = findNode(xPathExpression, root); if (node != null && node instanceof Element) { return (Element) node; } return null; } /** * Checks in under a given root element whether it can find a child node * which matches the XPath expression supplied. Returns {@link Node} if * exists. * * Please note that the XPath parser used is NOT namespace aware. So if you * want to find a element <beans><sec:http> you need to use the following * XPath expression '/beans/http'. * * @param xPathExpression the XPath expression (required) * @param root the parent DOM element (required) * @return the Node if discovered (null if not found) */ public static Node findNode(String xPathExpression, Element root) { Assert.hasText(xPathExpression, "XPath expression required"); Assert.notNull(root, "Root element required"); Node node = null; try { XPathExpression expr = compiledExpressionCache.get(xPathExpression); if (expr == null) { expr = xpath.compile(xPathExpression); compiledExpressionCache.put(xPathExpression, expr); } node = (Node) expr.evaluate(root, XPathConstants.NODE); } catch (XPathExpressionException e) { throw new IllegalArgumentException("Unable evaluate XPath expression", e); } return node; } /** * Checks in under a given root element whether it can find a child element * which matches the name supplied. Returns {@link Element} if exists. * * @param name the Element name (required) * @param root the parent DOM element (required) * @return the Element if discovered */ public static Element findFirstElementByName(String name, Element root) { Assert.hasText(name, "Element name required"); Assert.notNull(root, "Root element required"); return (Element) root.getElementsByTagName(name).item(0); } /** * Checks in under a given root element whether it can find a child element * which matches the XPath expression supplied. The {@link Element} must * exist. Returns {@link Element} if exists. * * Please note that the XPath parser used is NOT namespace aware. So if you * want to find a element <beans><sec:http> you need to use the following * XPath expression '/beans/http'. * * @param xPathExpression the XPath expression (required) * @param root the parent DOM element (required) * @return the Element if discovered (never null; an exception is thrown if cannot be found) */ public static Element findRequiredElement(String xPathExpression, Element root) { Assert.hasText(xPathExpression, "XPath expression required"); Assert.notNull(root, "Root element required"); Element element = findFirstElement(xPathExpression, root); Assert.notNull(element, "Unable to obtain required element '" + xPathExpression + "' from element '" + root + "'"); return element; } /** * Checks in under a given root element whether it can find a child elements * which match the XPath expression supplied. Returns a {@link List} of * {@link Element} if they exist. * * Please note that the XPath parser used is NOT namespace aware. So if you * want to find a element <beans><sec:http> you need to use the following * XPath expression '/beans/http'. * * @param xPathExpression the xPathExpression * @param root the parent DOM element * @return a {@link List} of type {@link Element} if discovered, otherwise an empty list (never null) */ public static List<Element> findElements(String xPathExpression, Element root) { List<Element> elements = new ArrayList<Element>(); NodeList nodes = null; try { XPathExpression expr = compiledExpressionCache.get(xPathExpression); if (expr == null) { expr = xpath.compile(xPathExpression); compiledExpressionCache.put(xPathExpression, expr); } nodes = (NodeList) expr.evaluate(root, XPathConstants.NODESET); } catch (XPathExpressionException e) { throw new IllegalArgumentException("Unable evaluate xpath expression", e); } for (int i = 0, n = nodes.getLength(); i < n; i++) { elements.add((Element) nodes.item(i)); } return elements; } /** * Checks for a given element whether it can find an attribute which matches the * XPath expression supplied. Returns {@link Node} if exists. * * @param xPathExpression the xPathExpression (required) * @param element (required) * @return the Node if discovered (null if not found) */ public static Node findFirstAttribute(String xPathExpression, Element element) { Node attr = null; try { XPathExpression expr = compiledExpressionCache.get(xPathExpression); if (expr == null) { expr = xpath.compile(xPathExpression); compiledExpressionCache.put(xPathExpression, expr); } attr = (Node) expr.evaluate(element, XPathConstants.NODE); } catch (XPathExpressionException e) { throw new IllegalArgumentException("Unable evaluate xpath expression", e); } return attr; } /** * @return a transformer that indents entries by 4 characters (never null) */ public static final Transformer createIndentingTransformer() { Transformer transformer; try { transformerFactory.setAttribute("indent-number", 4); transformer = transformerFactory.newTransformer(); } catch (Exception e) { throw new IllegalStateException(e); } transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); return transformer; } /** * @return a new document builder (never null) */ public static final DocumentBuilder getDocumentBuilder() { // factory.setNamespaceAware(true); try { return factory.newDocumentBuilder(); } catch (ParserConfigurationException e) { throw new IllegalStateException(e); } } /** * Removes empty text nodes from the specified node * * @param node the element where empty text nodes will be removed */ public static void removeTextNodes(Node node) { if (node == null) { return; } NodeList children = node.getChildNodes(); for (int i = children.getLength() - 1; i >= 0; i--) { Node child = children.item(i); switch (child.getNodeType()) { case Node.ELEMENT_NODE: removeTextNodes((Element) child); break; case Node.CDATA_SECTION_NODE: case Node.TEXT_NODE: if (!StringUtils.hasText(child.getNodeValue())) { node.removeChild(child); } break; } } } /** * Returns the root element of an addon's configuration file. * * @param clazz which owns the configuration * @param configurationPath the path of the configuration file. * @return the configuration root element */ public static Element getConfiguration(Class<?> clazz, String configurationPath) { InputStream templateInputStream = TemplateUtils.getTemplate(clazz, configurationPath); Assert.notNull(templateInputStream, "Could not acquire " + configurationPath + " file"); Document configurationDocument; try { configurationDocument = getDocumentBuilder().parse(templateInputStream); } catch (Exception e) { throw new IllegalStateException(e); } return configurationDocument.getDocumentElement(); } /** * Returns the root element of an addon's configuration file. * * @param clazz which owns the configuration * @return the configuration root element */ public static Element getConfiguration(Class<?> clazz) { return getConfiguration(clazz, "configuration.xml"); } /** * Converts a XHTML compliant id (used in jspx) to a CSS3 selector spec compliant id. In that * it will replace all '.,:,-' to '_' * * @param proposed Id * @return cleaned up Id */ public static String convertId(String proposed) { return proposed.replaceAll("[:\\.-]", "_"); } /** * Checks the presented element for illegal characters that could cause malformed XML. * * @param element the content of the XML element * @throws IllegalArgumentException if the element is null, has no text or contains illegal characters */ public static void assertElementLegal(String element) { if (!StringUtils.hasText(element)) { throw new IllegalArgumentException("Element required"); } // Note regular expression for legal characters found to be x5 slower in profiling than this approach char[] value = element.toCharArray(); for (int i = 0; i < value.length; i++) { char c = value[i]; if (' ' == c || '*' == c || '>' == c || '<' == c || '!' == c || '@' == c || '%' == c || '^' == c || '?' == c || '(' == c || ')' == c || '~' == c || '`' == c || '{' == c || '}' == c || '[' == c || ']' == c || '|' == c || '\\' == c || '\'' == c || '+' == c) { throw new IllegalArgumentException("Illegal name '" + element + "' (illegal character)"); } } } }