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

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

Go to download

The coolest XML library for Java around. Define typesafe views (projections) to xml. Use XPath to read and write XML. Bind XML to Java collections. Requires at least Java6, supports Java8 features and has no further runtime dependencies.

There is a newer version: 1.4.24
Show newest version
/**
 *  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)); } 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) { 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) { 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); } } }