Package org.springframework.roo.support.util

Source Code of org.springframework.roo.support.util.XmlUtils

package org.springframework.roo.support.util;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
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.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
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;
import org.xml.sax.SAXException;

/**
* Utilities related to XML usage.
*
* @author Stefan Schmidt
* @author Ben Alex
* @author Alan Stewart
* @author Andrew Swan
* @since 1.0
*/
public final class XmlUtils {

    private static final Map<String, XPathExpression> COMPILED_EXPRESSION_CACHE = new HashMap<String, XPathExpression>();
    private static final DocumentBuilderFactory FACTORY = DocumentBuilderFactory
            .newInstance();
    private static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory
            .newInstance();
    private static final XPath XPATH = XPathFactory.newInstance().newXPath();

    /**
     * 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(final String element) {
        if (StringUtils.isBlank(element)) {
            throw new IllegalArgumentException("Element required");
        }

        // Note regular expression for legal characters found to be x5 slower in
        // profiling than this approach
        final char[] value = element.toCharArray();
        for (final char c : value) {
            if (' ' == 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)");
            }
        }
    }

    /**
     * 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) {
        Validate.notNull(node1, "First node required");
        Validate.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 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(final String proposed) {
        return proposed.replaceAll("[:\\.-]", "_");
    }

    /**
     * @return a transformer that indents entries by 4 characters (never null)
     */
    public static Transformer createIndentingTransformer() {
        Transformer transformer;
        try {
            TRANSFORMER_FACTORY.setAttribute("indent-number", 4);
            transformer = TRANSFORMER_FACTORY.newTransformer();
        }
        catch (final Exception e) {
            throw new IllegalStateException(e);
        }
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.setOutputProperty(
                "{http://xml.apache.org/xslt}indent-amount", "4");
        return transformer;
    }

    /**
     * Creates an {@link Element} containing the given text
     *
     * @param document the document to contain the new element
     * @param tagName the element's tag name (required)
     * @param text the text to set; can be <code>null</code> for none
     * @return a non-<code>null</code> element
     * @since 1.2.0
     */
    public static Element createTextElement(final Document document,
            final String tagName, final String text) {
        final Element element = document.createElement(tagName);
        element.setTextContent(text);
        return element;
    }

    /**
     * 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(
            final OutputStream outputStream)
            throws UnsupportedEncodingException {
        final Writer writer;
        if (IOUtils.LINE_SEPARATOR.equals("\r\n")) {
            writer = new OutputStreamWriter(outputStream, "ISO-8859-1") {
                @Override
                public void write(final char[] cbuf, final int off,
                        final 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]);
                        }
                    }
                }

                @Override
                public void write(final int c) throws IOException {
                    if (c != '\r') {
                        super.write(c);
                    }
                }

                @Override
                public void write(final String str, final int off, final int len)
                        throws IOException {
                    final String orig = str.substring(off, off + len);
                    final String filtered = orig.replace("\r\n", "\n");
                    final 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 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(final String xPathExpression,
            final Element root) {
        final List<Element> elements = new ArrayList<Element>();
        NodeList nodes = null;

        try {
            XPathExpression expr = COMPILED_EXPRESSION_CACHE
                    .get(xPathExpression);
            if (expr == null) {
                expr = XPATH.compile(xPathExpression);
                COMPILED_EXPRESSION_CACHE.put(xPathExpression, expr);
            }
            nodes = (NodeList) expr.evaluate(root, XPathConstants.NODESET);
        }
        catch (final 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(final String xPathExpression,
            final Element element) {
        Node attr = null;
        try {
            XPathExpression expr = COMPILED_EXPRESSION_CACHE
                    .get(xPathExpression);
            if (expr == null) {
                expr = XPATH.compile(xPathExpression);
                COMPILED_EXPRESSION_CACHE.put(xPathExpression, expr);
            }
            attr = (Node) expr.evaluate(element, XPathConstants.NODE);
        }
        catch (final XPathExpressionException e) {
            throw new IllegalArgumentException(
                    "Unable evaluate xpath expression", e);
        }
        return attr;
    }

    /**
     * Searches the given parent element for a child element matching the given
     * XPath expression. Please note that the XPath parser used is NOT namespace
     * aware. So if you want to find an element
     * <code>&lt;beans&gt;&lt;sec:http&gt;</code>, you need to use the following
     * XPath expression '/beans/http'.
     *
     * @param xPathExpression the xPathExpression (required)
     * @param parent the parent DOM element (required)
     * @return the Element if discovered (null if no such {@link Element} found)
     */
    public static Element findFirstElement(final String xPathExpression,
            final Node parent) {
        final Node node = findNode(xPathExpression, parent);
        if (node instanceof Element) {
            return (Element) node;
        }
        return null;
    }

    /**
     * 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
     * @deprecated use {@link DomUtils#findFirstElementByName(String, Element)}
     *             instead
     */
    @Deprecated
    public static Element findFirstElementByName(final String name,
            final Element root) {
        return DomUtils.findFirstElementByName(name, root);
    }

    /**
     * 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 <code>&lt;beans&gt;&lt;sec:http&gt;</code>,
     * you need to use the XPath expression '<code>/beans/http</code>'.
     *
     * @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(final String xPathExpression, final Node root) {
        Validate.notBlank(xPathExpression, "XPath expression required");
        Validate.notNull(root, "Root element required");
        Node node = null;
        try {
            XPathExpression expr = COMPILED_EXPRESSION_CACHE
                    .get(xPathExpression);
            if (expr == null) {
                expr = XPATH.compile(xPathExpression);
                COMPILED_EXPRESSION_CACHE.put(xPathExpression, expr);
            }
            node = (Node) expr.evaluate(root, XPathConstants.NODE);
        }
        catch (final XPathExpressionException e) {
            throw new IllegalArgumentException(
                    "Unable evaluate XPath expression '" + xPathExpression
                            + "'", e);
        }
        return node;
    }

    /**
     * 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(final String xPathExpression,
            final Element root) {
        Validate.notBlank(xPathExpression, "XPath expression required");
        Validate.notNull(root, "Root element required");
        final Element element = findFirstElement(xPathExpression, root);
        Validate.notNull(element, "Unable to obtain required element '"
                + xPathExpression + "' from element '" + root + "'");
        return element;
    }

    /**
     * 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(final Class<?> clazz) {
        return getRootElement(clazz, "configuration.xml");
    }

    /**
     * @return a new document builder (never null)
     */
    public static DocumentBuilder getDocumentBuilder() {
        // factory.setNamespaceAware(true);
        try {
            return FACTORY.newDocumentBuilder();
        }
        catch (final ParserConfigurationException e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * Returns the root element of the given XML file.
     *
     * @param clazz the class from whose package to open the file (required)
     * @param xmlFilePath the path of the XML file relative to the given class'
     *            package (required)
     * @return a non-<code>null</code> element
     * @see Document#getDocumentElement()
     */
    public static Element getRootElement(final Class<?> clazz,
            final String xmlFilePath) {
        final InputStream inputStream = FileUtils.getInputStream(clazz,
                xmlFilePath);
        Validate.notNull(inputStream, "Could not open the file '%s'",
                xmlFilePath);
        return readXml(inputStream).getDocumentElement();
    }

    public static String getTextContent(final String path,
            final Element parentElement) {
        return getTextContent(path, parentElement, null);
    }

    public static String getTextContent(final String path,
            final Element parentElement, final String valueIfNull) {
        final Element element = XmlUtils.findFirstElement(path, parentElement);
        if (element != null) {
            return element.getTextContent();
        }
        return valueIfNull;
    }

    /**
     * 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(final Node node) {
        try {
            final StringWriter writer = new StringWriter();
            createIndentingTransformer().transform(new DOMSource(node),
                    new StreamResult(writer));
            return writer.toString();
        }
        catch (final TransformerException e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * 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.
     * @throws IllegalStateException if the stream could not be read or parsed
     */
    public static Document readXml(InputStream inputStream) {
        Validate.notNull(inputStream, "InputStream required");
        try {
            if (!(inputStream instanceof BufferedInputStream)) {
                inputStream = new BufferedInputStream(inputStream);
            }
            return getDocumentBuilder().parse(inputStream);
        }
        catch (final Exception e) {
            throw new IllegalStateException(e);
        }
        finally {
            IOUtils.closeQuietly(inputStream);
        }
    }

    /**
     * Removes empty text nodes from the specified node
     *
     * @param node the element where empty text nodes will be removed
     * @deprecated use {@link DomUtils#removeTextNodes(Node)} instead
     */
    @Deprecated
    public static void removeTextNodes(final Node node) {
        DomUtils.removeTextNodes(node);
    }

    /**
     * Returns the given XML as the root {@link Element} of a new
     * {@link Document}
     *
     * @param xml the XML to convert; can be blank
     * @return <code>null</code> if the given XML is blank
     * @since 1.2.0
     */
    public static Element stringToElement(final String xml) {
        if (StringUtils.isBlank(xml)) {
            return null;
        }
        try {
            return FACTORY.newDocumentBuilder()
                    .parse(new ByteArrayInputStream(xml.getBytes()))
                    .getDocumentElement();
        }
        catch (final IOException e) {
            throw new IllegalStateException(e);
        }
        catch (final ParserConfigurationException e) {
            throw new IllegalStateException(e);
        }
        catch (final SAXException e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * 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(final OutputStream outputStream,
            final Document document) {
        // Note that the "format-pretty-print" DOM configuration parameter can
        // only be set in JDK 1.6+.
        final 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 (final NoSuchMethodError nsme) {
                // Fall back to default LS
                DOMImplementationRegistry registry = null;
                try {
                    registry = DOMImplementationRegistry.newInstance();
                }
                catch (final 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) {
                final LSSerializer lsSerializer = domImplementationLS
                        .createLSSerializer();
                final DOMConfiguration domConfiguration = lsSerializer
                        .getDomConfig();
                if (domConfiguration.canSetParameter("format-pretty-print",
                        Boolean.TRUE)) {
                    lsSerializer.getDomConfig().setParameter(
                            "format-pretty-print", Boolean.TRUE);
                    final LSOutput lsOutput = domImplementationLS
                            .createLSOutput();
                    lsOutput.setEncoding("UTF-8");
                    lsOutput.setByteStream(outputStream);
                    try {
                        lsSerializer.write(document, lsOutput);
                    }
                    catch (final LSException lse) {
                        throw new IllegalStateException(lse);
                    }
                    finally {
                        IOUtils.closeQuietly(outputStream);
                    }
                }
                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);
        }
    }

    /**
     * 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 void writeXml(final OutputStream outputStream,
            final 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 void writeXml(final Transformer transformer,
            OutputStream outputStream, final Document document) {
        Validate.notNull(transformer, "Transformer required");
        Validate.notNull(outputStream, "OutputStream required");
        Validate.notNull(document, "Document required");

        transformer.setOutputProperty(OutputKeys.METHOD, "xml");
        try {
            if (!(outputStream instanceof BufferedOutputStream)) {
                outputStream = new BufferedOutputStream(outputStream);
            }
            final StreamResult streamResult = createUnixStreamResultForEntry(outputStream);
            transformer.transform(new DOMSource(document), streamResult);
        }
        catch (final Exception e) {
            throw new IllegalStateException(e);
        }
        finally {
            IOUtils.closeQuietly(outputStream);
        }
    }

    /**
     * Constructor is private to prevent instantiation
     */
    private XmlUtils() {
    }
}
TOP

Related Classes of org.springframework.roo.support.util.XmlUtils

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.