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.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.xml.XMLConstants;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Attr;
import org.w3c.dom.DOMException;
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.w3c.dom.Text;
import org.xmlbeam.XBProjector;
import org.xmlbeam.exceptions.XBException;

/**
 * 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 {

    /**
     * Null safe comparator for DOM nodes.
     */
    private static final Comparator ATTRIBUTE_NODE_COMPARATOR = new Comparator() {
        private int compareMaybeNull(final Comparable a, final 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(final Node o1, final 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;
        }
    };

    /**
     * Parse namespace prefixes defined anywhere in the document.
     *
     * @param document
     *            source document.
     * @return map with prefix->uri relationships.
     */
    public static Map getNamespaceMapping(final Document document) {
        Map map = new HashMap();
        map.put("xmlns", "http://www.w3.org/2000/xmlns/");
        map.put("xml", "http://www.w3.org/XML/1998/namespace");
//      if (childName.equals("xmlns") || childName.startsWith("xmlns:")) {
//      return "http://www.w3.org/2000/xmlns/";
//  }
//  if (childName.startsWith("xml:")) {
//      return "http://www.w3.org/XML/1998/namespace";
//  }

        Element root = document.getDocumentElement();
        if (root == null) {
            // No document, no namespaces.
            return map;
        }

        fillNSMapWithPrefixesDeclaredInElement(map, root);
        return map;
    }

    /**
     * Search for prefix definitions in element and all children. There still is an issue for
     * documents that use the same prefix on differen namespaces in disjunct subtrees. This might be
     * possible but we won't support this. Same is with declaring multiple default namespaces.
     * XMLBeams behaviour will be undefined in that case. There is a workaround by defining a custom
     * namespace/prefix mapping, so the effort to support this is not justified.
     *
     * @param nsMap
     * @param element
     * @throws DOMException
     */
    private static void fillNSMapWithPrefixesDeclaredInElement(final Map nsMap, final Element element) throws DOMException {
        NamedNodeMap attributes = element.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())) {
                nsMap.put("xbdefaultns", attribute.getNodeValue());
                continue;
            }
            nsMap.put(attribute.getLocalName(), attribute.getNodeValue());
        }
        NodeList childNodes = element.getChildNodes();
        for (Node n : nodeListToIterator(childNodes)) {
            if (n.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }
            fillNSMapWithPrefixesDeclaredInElement(nsMap, (Element) n);
        }
    }

    /**
     * Replace the current root element. If element is null, the current root element will be
     * removed.
     *
     * @param document
     * @param element
     */
    public static void setDocumentElement(final Document document, final 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(final Node a, final 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(final NodeList a, final 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(final NamedNodeMap a, final 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(final 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(final 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; } /** * @param element * @param attributeName * @param value */ public static void setOrRemoveAttribute(final Element element, final String attributeName, final String value) { if (value == null) { element.removeAttribute(attributeName); return; } element.setAttribute(attributeName, value); } /** * @param node * @param newName * @return a new Element instance with desired name and content. */ @SuppressWarnings("unchecked") public static T renameNode(final T node, final String newName) { if (node instanceof Attr) { Attr attributeNode = (Attr) node; final Element owner = attributeNode.getOwnerElement(); if (owner == null) { throw new IllegalArgumentException("Attribute has no owner " + node); } owner.removeAttributeNode(attributeNode); owner.setAttribute(newName, attributeNode.getValue()); return (T) owner.getAttributeNode(newName); } if (node instanceof Element) { Element element = (Element) node; Node parent = element.getParentNode(); 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); } NamedNodeMap attributes = element.getAttributes(); for (int i = 0; i < attributes.getLength(); ++i) { newElement.setAttributeNode((Attr) attributes.item(i).cloneNode(true)); } if (parent != null) { parent.replaceChild(newElement, element); } return (T) newElement; } throw new IllegalArgumentException("Can not rename node " + node); } /** * @param ownerDocument * @param node */ public static void ensureOwnership(final Document ownerDocument, final Node node) { if (ownerDocument != node.getOwnerDocument()) { ownerDocument.adoptNode(node); } } /** * @param documentOrElement * @return document that owns the given node */ public static Document getOwnerDocumentFor(final 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(final String elementName) { if (elementName.contains(":")) { return elementName.replaceAll(":.*", ""); } return ""; } /** * @param domNode */ public static void trim(final Node domNode) { assert domNode != null; assert (Node.TEXT_NODE != domNode.getNodeType()); List removeMe = new LinkedList(); NodeList childNodes = domNode.getChildNodes(); for (Node child : nodeListToIterator(childNodes)) { if (Node.TEXT_NODE == child.getNodeType()) { if ((child.getNodeValue() == null) || child.getNodeValue().trim().isEmpty()) { removeMe.add((Text) child); } continue; } trim(child); } for (Text node : removeMe) { Node parent = node.getParentNode(); if (parent != null) { parent.removeChild(node); } } } /** * @param childNodes * @return */ private static Iterable nodeListToIterator(final NodeList nodeList) { return new Iterable() { @Override public Iterator iterator() { return new Iterator() { private int pos = 0; @Override public boolean hasNext() { return nodeList.getLength() > pos; } @Override public Node next() { return nodeList.item(pos++); } @Override public void remove() { throw new IllegalStateException(); } }; } }; } /** * @param node * @return either a List with this node, or an empty list if node is null. */ public static List asList(final T node) { if (node == null) { return Collections.emptyList(); } return Collections.singletonList(node); } /** * @param previous * @param newNode */ public static void replaceElement(final Element previous, final Element newNode) { assert previous.getParentNode() != null; Element parent = (Element) previous.getParentNode(); Document document = DOMHelper.getOwnerDocumentFor(parent); DOMHelper.ensureOwnership(document, newNode); parent.replaceChild(newNode, previous); } /** * Simply removes all child nodes. * * @param node */ public static void removeAllChildren(final Node node) { assert node != null; assert node.getNodeType() != Node.ATTRIBUTE_NODE; if (node.getNodeType() == Node.DOCUMENT_TYPE_NODE) { Element documentElement = ((Document) node).getDocumentElement(); if (documentElement != null) { ((Document) node).removeChild(documentElement); } return; } if (node.getNodeType() == Node.ELEMENT_NODE) { Element element = (Element) node; for (Node n = element.getFirstChild(); n != null; n = element.getFirstChild()) { element.removeChild(n); } } } /** * @param attributeNode */ public static void removeAttribute(final Attr attributeNode) { if (attributeNode == null) { return; } final Element owner = attributeNode.getOwnerElement(); if (owner == null) { return; } owner.removeAttributeNode(attributeNode); } /** * @param node */ private static void removeNode(final Node node) { if (node == null) { return; } while (node.hasChildNodes()) { removeNode(node.getFirstChild()); } final Node parent = node.getParentNode(); if (parent == null) { return; } parent.removeChild(node); } /** * @param parentElement * @param o * @return the new clone */ public static Node appendClone(final Element parentElement, final Node o) { Node clone = o.cloneNode(true); ensureOwnership(DOMHelper.getOwnerDocumentFor(parentElement), clone); parentElement.appendChild(clone); return clone; } /** * @param existingNodes */ public static void removeNodes(final Iterable existingNodes) { for (Node e : existingNodes) { removeNode(e); } } /** * @param projector * @param domNode * @return rendered XML as String */ public static String toXMLString(final XBProjector projector, final Node domNode) { if (domNode == null) { return ""; } if (domNode.getNodeType() == Node.ATTRIBUTE_NODE) { return domNode.toString(); } try { final StringWriter writer = new StringWriter(); projector.config().createTransformer().transform(new DOMSource(domNode), new StreamResult(writer)); final String output = writer.getBuffer().toString(); return output; } catch (TransformerConfigurationException e) { throw new XBException("Error while creating transformer",e); } catch (TransformerException e) { throw new XBException("Error while transforming document",e); } } /** * @param item * @return Text content of this node, without child content. */ public static String directTextContent(final Node item) { if ((item.getNodeType() == Node.TEXT_NODE)||(item.getNodeType() == Node.CDATA_SECTION_NODE)) { return item.getNodeValue(); } NodeList childNodes = item.getChildNodes(); if (childNodes == null) { return null; } StringBuilder sb = new StringBuilder(); for (int i = 0; i < childNodes.getLength(); ++i) { Node child = childNodes.item(i); if ((child.getNodeType() != Node.TEXT_NODE)&&(child.getNodeType() != Node.CDATA_SECTION_NODE)) { continue; } sb.append(child.getNodeValue()); } return sb.toString(); } /** * Set text content of given element without removing existing child nodes. Text nodes are added * after child element nodes always. * * @param elementToChange * @param asString */ public static void setDirectTextContent(final Node elementToChange, final String asString) { assert elementToChange.getNodeType() != Node.DOCUMENT_NODE; if (Node.ATTRIBUTE_NODE == elementToChange.getNodeType()) { elementToChange.setTextContent(asString); return; } List nodes = new LinkedList(); List nodes2 = new LinkedList(); for (Node n : nodeListToIterator(elementToChange.getChildNodes())) { if (Node.TEXT_NODE == n.getNodeType()) { continue; } nodes.add(n); } if ((asString != null) && (!asString.isEmpty())) { elementToChange.setTextContent(asString); for (Node n : nodeListToIterator(elementToChange.getChildNodes())) { if (Node.TEXT_NODE != n.getNodeType()) { continue; } nodes.add(n); } } removeAllChildren(elementToChange); for (Node n : nodes) { elementToChange.appendChild(n); } for (Node n : nodes2) { elementToChange.appendChild(n); } } }