All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.xmlbeam.util.intern.DOMHelper Maven / Gradle / Ivy

/**
 *  Copyright 2012 Sven Ewald
 *
 *  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 org.xmlbeam.util.intern;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import java.io.IOException;
import java.io.InputStream;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;

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 org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xmlbeam.util.IOHelper;

/**
 * A set of tiny helper methods internally used in the projection framework. This methods are
 * not part of the public framework API and might change in minor version updates.
 * 
 * @author Sven Ewald
 */
public final class DOMHelper {

    private static final Comparator ATTRIBUTE_NODE_COMPARATOR = new Comparator() {
        private int compareMaybeNull(Comparable a, Object b) {
            if (a == b) {
                return 0;
            }
            if (a == null) {
                return -1;
            }
            if (b == null) {
                return 1;
            }
            return a.compareTo(b);
        }

        @Override
        public int compare(Node o1, Node o2) {
            Comparable[] c1 = getNodeAttributes(o1);
            Comparable[] c2 = getNodeAttributes(o2);
            assert c1.length == c2.length;
            for (int i = 0; i < c1.length; ++i) {
                int result = compareMaybeNull(c1[i], c2[i]);
                if (result != 0) {
                    return result;
                }
            }
            return 0;
        }
    };

    /**
     * Remove all child elements with given node name. If nodeSelector is "*", then remove all
     * children.
     * 
     * @param element
     * @param nodeSelector
     */
    public static void removeAllChildrenBySelector(Node element, String nodeSelector) {
        assert nodeSelector != null;
        NodeList nodeList = element.getChildNodes();
        List toBeRemoved = new LinkedList();
        if ("*".equals(nodeSelector)) {
            for (int i = 0; i < nodeList.getLength(); ++i) {
                toBeRemoved.add(nodeList.item(i));
            }
        } else {
            for (int i = 0; i < nodeList.getLength(); ++i) {
                Node node = nodeList.item(i);
                if (Node.ELEMENT_NODE != node.getNodeType()) {
                    continue;
                }
                if (selectorMatches(nodeSelector, (Element) node)) {
                    toBeRemoved.add(nodeList.item(i));
                }
            }
        }
        for (Node e : toBeRemoved) {
            element.removeChild(e);
        }
    }

    /**
     * @param documentBuilder
     * @param url
     * @param requestProperties
     * @param resourceAwareClass
     * @return new document instance
     * @throws IOException
     */
    @SuppressWarnings("unchecked")
    public static Document getDocumentFromURL(DocumentBuilder documentBuilder, final String url, Map requestProperties, final Class resourceAwareClass) throws IOException {
        try {
            if (url.startsWith("resource://")) {
                InputStream is = resourceAwareClass.getResourceAsStream(url.substring("resource://".length()));
                InputSource source = new InputSource(is);
                // source.setEncoding("MacRoman");
                return documentBuilder.parse(source);
            }
            if (url.startsWith("http:") || url.startsWith("https:")) {
                return documentBuilder.parse(IOHelper.httpGet(url, requestProperties), url);
            }
            Document document = documentBuilder.parse(url);
            if (document == null) {
                throw new IOException("Document could not be created form uri " + url);
            }
            return document;
        } catch (SAXException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Parse namespace prefixes defined in the documents root element.
     * 
     * @param document
     *            source document.
     * @return map with prefix->uri relationships.
     */
    public static Map getNamespaceMapping(Document document) {
        Map map = new HashMap();
        Element root = document.getDocumentElement();
        if (root == null) {
            // No document, no namespaces.
            return Collections.emptyMap();
        }
        NamedNodeMap attributes = root.getAttributes();
        for (int i = 0; i < attributes.getLength(); i++) {
            Node attribute = attributes.item(i);
            if ((!XMLConstants.XMLNS_ATTRIBUTE.equals(attribute.getPrefix())) && (!XMLConstants.XMLNS_ATTRIBUTE.equals(attribute.getLocalName()))) {
                continue;
            }
            if (XMLConstants.XMLNS_ATTRIBUTE.equals(attribute.getLocalName())) {
                map.put("xbdefaultns", attribute.getNodeValue());
                continue;
            }
            map.put(attribute.getLocalName(), attribute.getNodeValue());
        }
        return map;
    }

    /**
     * Treat the given path as relative path from a base element to an element and return this
     * element. If any element on this path does not exist, create it.
     * @param document 
     * 
     * @param base
     *            the relative path will start here
     * @param pathToElement
     *            relative path to element.
     * @return element with absolute path.
     */
    public static Element ensureElementExists(final Document document, final Element base, final String pathToElement) {
        assert base != null;
        assert pathToElement != null;
        if ((pathToElement.isEmpty()) || (".".equals(pathToElement))) {
            return base;
        }
        Element element = base;
        String splitme = pathToElement.replaceAll("(^/)|(/$)", "");
        if (splitme.isEmpty()) {
            throw new IllegalArgumentException("Path must not be empty. I don't know which element to return.");
        }
        for (String expectedElementName : splitme.split("/")) {
            if (".".equals(expectedElementName)) {
                continue;
            }
            if ("..".equals(expectedElementName)) {
                Node parent = element.getParentNode();
                if ((parent == null) || (Node.ELEMENT_NODE != parent.getNodeType())) {
                    throw new IllegalArgumentException("Reference to parent node via '..' bounced out of scope.");
                }
                element = (Element) parent;
                continue;
            }
            if (expectedElementName.equals(element.getNodeName())) {
                continue;
            }

            String[] nameAndSelector = splitToNameAndSelector(expectedElementName);

            // String name = expectedElementName.replaceAll("\\[.*", "");
            // String selector = expectedElementName.contains("[") ?
// expectedElementName.replaceAll(".*\\[", "").replaceAll("\\]$", "") : "";

            Element child = findElementByTagNameAndSelector(element, nameAndSelector[0], nameAndSelector[1]);
            if (child == null) {
                // element = (Element)
// element.appendChild(document.createElement(expectedElementName));
                element = (Element) element.appendChild(createElementByTagNameAndSelector(document, nameAndSelector[0], nameAndSelector[1]));
                continue;
            }
   //         forceSelectorOnElement(document, nameAndSelector[1], child);
            element = child;
        }
        return element;
    }

    private static String[] splitToNameAndSelector(String nameAndSelector) {
        return new String[] {//
        nameAndSelector.replaceAll("\\[.*", ""),//
                nameAndSelector.contains("[") ? nameAndSelector.replaceAll(".*\\[", "").replaceAll("\\]$", "") : ""//
        };
    }

    /**
     * @param document
     * @param expectedElementName
     * @param selector
     * @return
     */
    private static Element createElementByTagNameAndSelector(final Document document, final String name, final String selector) {
        // Element element = document.createElement(name);
        final Element element = createElement(document, name);
        forceSelectorOnElement(document, selector, element);
        return element;
    }

    private static Element forceSelectorOnElement(final Document document, final String selector, final Element element) {
        if (selector.isEmpty()) {
            return element;
        }
        final String[] selectorValues = splitSelector(selector);
        if (selectorValues[0].startsWith("@")) {
            final String prefix = getPrefixOfQName(selectorValues[0].substring(1));
            if (prefix.isEmpty()) {
                element.setAttribute(selectorValues[0].substring(1), selectorValues[1]);
            } else {
                final String namespaceURI = ("xmlns".equals(prefix)) ? "http://www.w3.org/2000/xmlns/" : element.getNamespaceURI();
                element.setAttributeNS(namespaceURI, selectorValues[0].substring(1), selectorValues[1]);
            }
            return element;
        }
        // Element child = document.createElement(selectorValues[0]);
        final Element child = createElement(document, selectorValues[0]);
        child.setTextContent(selectorValues[1]);
        element.appendChild(child);
        return element;
    }

    /**
     * @param element
     * @param expectedElementName
     * @return
     */
    private static Element findElementByTagNameAndSelector(Element element, String name, String selector) {
        NodeList nodeList = element.getElementsByTagName(name);
        for (int i = 0; i < nodeList.getLength(); ++i) {
            if (selectorMatches(selector, (Element) nodeList.item(i))) {
                return (Element) nodeList.item(i);
            }
        }
        return null;
    }

    /**
     * @param selector
     * @param item
     * @return
     */
    private static boolean selectorMatches(String selector, Element item) {
        if (item == null) {
            return false;
        }
        if ((selector == null) || (selector.isEmpty())) {
            return true;
        }
        if (!selector.contains("=")) {
            if (selector.matches("^@.+")) {
                return item.hasAttribute(selector.substring(1));                
            }            
            return selector.equals(item.getNodeName());
        }
        String[] selectorValues = splitSelector(selector);
        if (selectorValues[0].startsWith("@")) {
            return selectorValues[1].equals(item.getAttribute(selectorValues[0].substring(1)));
        }
        NodeList nodeList = item.getElementsByTagName(selectorValues[0]);
        for (int i = 0; i < nodeList.getLength(); ++i) {
            if (selectorValues[1].equals(nodeList.item(i).getTextContent())) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param selector
     * @return
     */
    private static String[] splitSelector(String selector) {
        if (!selector.matches("@?[^=]+=[^=]+")) {
            return new String[] { selector, "" };
            // throw new
// IllegalArgumentException("When using a predicate to create elements, predicate expressions must assign values via '=' to an attribute or direct child element.");
        }
        selector = selector.replaceAll("(^\\[)|(\\]$)", "");
        String[] split = selector.split("=");
        split[1] = split[1].replaceAll("(^')|('$)", "");
        return split;
    }

    /**
     * Treat the given path as absolute path to an element and return this element. If any element
     * on this path does not exist, create it.
     * 
     * @param document
     *            document
     * @param pathToElement
     *            absolute path to element.
     * @return element with absolute path.
     */
    public static Element ensureElementExists(final Document document, final String pathToElement) {
        assert document != null;
        String splitme = pathToElement.replaceAll("(^/)|(/$)", "");
        if (splitme.isEmpty()) {
            throw new IllegalArgumentException("Path must not be empty. I don't know which element to return.");
        }
        Element element = document.getDocumentElement();
        final String[] nameAndSelector = splitToNameAndSelector(splitme.replaceAll("/.*", ""));
        if (element == null) { // No root element yet
            element = createElementByTagNameAndSelector(document, nameAndSelector[0], nameAndSelector[1]);
            // element = document.createElement(splitme.replaceAll("/.*", ""));
            document.appendChild(element);
        } else {
            forceSelectorOnElement(document, nameAndSelector[1], element);
        }

        return ensureElementExists(document, element, splitme.replaceFirst("[^/]*/", ""));
    }

    /**
     * Replace the current root element. If element is null, the current root element will be
     * removed.
     * 
     * @param document
     * @param element
     */
    public static void setDocumentElement(Document document, Element element) {
        Element documentElement = document.getDocumentElement();
        if (documentElement != null) {
            document.removeChild(documentElement);
        }
        if (element != null) {
            if (element.getOwnerDocument().equals(document)) {
                document.appendChild(element);
                return;
            }
            Node node = document.adoptNode(element);
            document.appendChild(node);
        }
    }

    /**
     * Implementation independent version of the Node.isEqualNode() method. Matches the same
     * algorithm as the nodeHashCode method. 
* Two nodes are equal if and only if the following conditions are satisfied: *
    *
  • The two nodes are of the same type.
  • *
  • The following string attributes are equal: nodeName, localName, * namespaceURI, prefix, nodeValue . This is: they are * both null, or they have the same length and are character for character * identical.
  • *
  • The attributes NamedNodeMaps are equal. This is: they are both * null, or they have the same length and for each node that exists in one map * there is a node that exists in the other map and is equal, although not necessarily at the * same index.
  • *
  • The childNodes NodeLists are equal. This is: they are both * null, or they have the same length and contain equal nodes at the same index. * Note that normalization can affect equality; to avoid this, nodes should be normalized before * being compared.
  • *
*
* For two DocumentType nodes to be equal, the following conditions must also be * satisfied: *
    *
  • The following string attributes are equal: publicId, systemId, * internalSubset.
  • *
  • The entities NamedNodeMaps are equal.
  • *
  • The notations NamedNodeMaps are equal.
  • *
* * @param a * @param b * @return true if and only if the nodes are equal in the manner explained above */ public static boolean nodesAreEqual(Node a, Node b) { if (a == b) { return true; } if ((a == null) || (b == null)) { return false; } if (!Arrays.equals(getNodeAttributes(a), getNodeAttributes(b))) { return false; } if (!namedNodeMapsAreEqual(a.getAttributes(), b.getAttributes())) { return false; } if (!nodeListsAreEqual(a.getChildNodes(), b.getChildNodes())) { return false; } return true; } /** * NodelLists are equal if and only if their size is equal and the containing nodes at the same * indexes are equal. * * @param a * @param b * @return */ private static boolean nodeListsAreEqual(NodeList a, NodeList b) { if (a == b) { return true; } if ((a == null) || (b == null)) { return false; } if (a.getLength() != b.getLength()) { return false; } for (int i = 0; i < a.getLength(); ++i) { if (!nodesAreEqual(a.item(i), b.item(i))) { return false; } } return true; } /** * NamedNodeMaps (e.g. the attributes of a node) are equal if for each containing node an equal * node exists in the other map. * * @param a * @param b * @return */ private static boolean namedNodeMapsAreEqual(NamedNodeMap a, NamedNodeMap b) { if (a == b) { return true; } if ((a == null) || (b == null)) { return false; } if (a.getLength() != b.getLength()) { return false; } List listA = new ArrayList(a.getLength()); List listB = new ArrayList(a.getLength()); for (int i = 0; i < a.getLength(); ++i) { listA.add(a.item(i)); listB.add(b.item(i)); } Collections.sort(listA, ATTRIBUTE_NODE_COMPARATOR); Collections.sort(listB, ATTRIBUTE_NODE_COMPARATOR); for (Node n1 : listA) { if (!nodesAreEqual(n1, listB.remove(0))) { return false; } } return true; } @SuppressWarnings("unchecked") private static Comparable[] getNodeAttributes(Node node) { return new Comparable[] { Short.valueOf(node.getNodeType()), node.getNodeName(), node.getLocalName(), node.getNamespaceURI(), node.getPrefix(), node.getNodeValue() }; } /** * hashcode() implementation that is compatible with equals(). * @param node * @return hash code for node */ public static int nodeHashCode(Node node) { assert node != null; int hash = 1 + node.getNodeType(); hash = hash * 17 + Arrays.hashCode(getNodeAttributes(node)); if (node.hasAttributes()) { NamedNodeMap nodeMap = node.getAttributes(); for (int i = 0; i < nodeMap.getLength(); ++i) { hash = 31 * hash + nodeHashCode(nodeMap.item(i)); } } if (node.hasChildNodes()) { NodeList childNodes = node.getChildNodes(); for (int i = 0; i < childNodes.getLength(); ++i) { hash = hash * 47 + nodeHashCode(childNodes.item(i)); } } return hash; } // public static void trimTextNodes(Node e) { // NodeList children = e.getChildNodes(); // for (int i = 0; i < children.getLength(); ++i) { // Node child = children.item(i); // if (Node.TEXT_NODE != child.getNodeType()) { // trimTextNodes(child); // continue; // } // String content = child.getNodeValue(); // if (content != null) { // content = content.trim(); // if (!content.isEmpty()) { // child.setNodeValue(content); // continue; // } // } // e.removeChild(child); // } // } /** * @param element * @param attributeName * @param value */ public static void setOrRemoveAttribute(Element element, String attributeName, String value) { if (value == null) { element.removeAttribute(attributeName); return; } element.setAttribute(attributeName, value); } /** * @param element * @param newName * @return a new Element instance with desired name and content. */ public static Element renameElement(Element element, String newName) { Document document = element.getOwnerDocument(); // Element newElement = document.createElement(newName); final Element newElement = createElement(document, newName); NodeList nodeList = element.getChildNodes(); List toBeMoved = new LinkedList(); for (int i = 0; i < nodeList.getLength(); ++i) { toBeMoved.add(nodeList.item(i)); } for (Node e : toBeMoved) { element.removeChild(e); newElement.appendChild(e); } return newElement; } /** * @param ownerDocument * @param element */ public static void ensureOwnership(Document ownerDocument, Element element) { if (ownerDocument != element.getOwnerDocument()) { ownerDocument.adoptNode(element); } } /** * @param documentOrElement * @return document that owns the given node */ public static Document getOwnerDocumentFor(Node documentOrElement) { if (Node.DOCUMENT_NODE == documentOrElement.getNodeType()) { return (Document) documentOrElement; } return documentOrElement.getOwnerDocument(); } private static Element createElement(final Document document, final String elementName) { final String prefix = getPrefixOfQName(elementName);// .replaceAll("(:.*)|([^:])*", ""); final String namespaceURI = prefix.isEmpty() ? null : document.lookupNamespaceURI(prefix); final Element element; if (namespaceURI == null) { element = document.createElement(elementName); } else { element = document.createElementNS(namespaceURI, elementName); } return element; } /** * @param elementName * @return */ private static String getPrefixOfQName(String elementName) { if (elementName.contains(":")) { return elementName.replaceAll(":.*", ""); } return ""; } }