    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *     http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017
    018package org.apache.commons.jxpath.ri.model.dom;
    019
    020import java.util.HashMap;
    021import java.util.Locale;
    022import java.util.Map;
    023
    024import org.apache.commons.jxpath.JXPathAbstractFactoryException;
    025import org.apache.commons.jxpath.JXPathContext;
    026import org.apache.commons.jxpath.JXPathException;
    027import org.apache.commons.jxpath.Pointer;
    028import org.apache.commons.jxpath.ri.Compiler;
    029import org.apache.commons.jxpath.ri.NamespaceResolver;
    030import org.apache.commons.jxpath.ri.QName;
    031import org.apache.commons.jxpath.ri.compiler.NodeNameTest;
    032import org.apache.commons.jxpath.ri.compiler.NodeTest;
    033import org.apache.commons.jxpath.ri.compiler.NodeTypeTest;
    034import org.apache.commons.jxpath.ri.compiler.ProcessingInstructionTest;
    035import org.apache.commons.jxpath.ri.model.NodeIterator;
    036import org.apache.commons.jxpath.ri.model.NodePointer;
    037import org.apache.commons.jxpath.ri.model.beans.NullPointer;
    038import org.apache.commons.jxpath.util.TypeUtils;
    039import org.w3c.dom.Attr;
    040import org.w3c.dom.Comment;
    041import org.w3c.dom.Document;
    042import org.w3c.dom.Element;
    043import org.w3c.dom.NamedNodeMap;
    044import org.w3c.dom.Node;
    045import org.w3c.dom.NodeList;
    046import org.w3c.dom.ProcessingInstruction;
    047
    048/**
    049 * A Pointer that points to a DOM node. Because a DOM Node is not guaranteed Serializable, a DOMNodePointer instance may likewise not be properly Serializable.
    050 */
    051public class DOMNodePointer extends NodePointer {
    052
    053    private static final long serialVersionUID = -8751046933894857319L;
    054    /** XML namespace URI */
    055    public static final String XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
    056    /** XMLNS namespace URI */
    057    public static final String XMLNS_NAMESPACE_URI = "http://www.w3.org/2000/xmlns/";
    058
    059    /**
    060     * Test string equality.
    061     *
    062     * @param s1 String 1
    063     * @param s2 String 2
    064     * @return true if == or .equals()
    065     */
    066    private static boolean equalStrings(String s1, String s2) {
    067        if (s1 == s2) {
    068            return true;
    069        }
    070        s1 = s1 == null ? "" : s1.trim();
    071        s2 = s2 == null ? "" : s2.trim();
    072        return s1.equals(s2);
    073    }
    074
    075    /**
    076     * Find the nearest occurrence of the specified attribute on the specified and enclosing elements.
    077     *
    078     * @param n        current node
    079     * @param attrName attribute name
    080     * @return attribute value
    081     */
    082    protected static String findEnclosingAttribute(Node n, final String attrName) {
    083        while (n != null) {
    084            if (n.getNodeType() == Node.ELEMENT_NODE) {
    085                final Element e = (Element) n;
    086                final String attr = e.getAttribute(attrName);
    087                if (attr != null && !attr.isEmpty()) {
    088                    return attr;
    089                }
    090            }
    091            n = n.getParentNode();
    092        }
    093        return null;
    094    }
    095
    096    /**
    097     * Gets the local name of the specified node.
    098     *
    099     * @param node node to check
    100     * @return String local name
    101     */
    102    public static String getLocalName(final Node node) {
    103        final String localName = node.getLocalName();
    104        if (localName != null) {
    105            return localName;
    106        }
    107        final String name = node.getNodeName();
    108        final int index = name.lastIndexOf(':');
    109        return index < 0 ? name : name.substring(index + 1);
    110    }
    111
    112    /**
    113     * Gets the ns uri of the specified node.
    114     *
    115     * @param node Node to check
    116     * @return String ns uri
    117     */
    118    public static String getNamespaceURI(Node node) {
    119        if (node instanceof Document) {
    120            node = ((Document) node).getDocumentElement();
    121        }
    122        final Element element = (Element) node;
    123        String uri = element.getNamespaceURI();
    124        if (uri == null) {
    125            final String prefix = getPrefix(node);
    126            final String qname = prefix == null ? "xmlns" : "xmlns:" + prefix;
    127            Node aNode = node;
    128            while (aNode != null) {
    129                if (aNode.getNodeType() == Node.ELEMENT_NODE) {
    130                    final Attr attr = ((Element) aNode).getAttributeNode(qname);
    131                    if (attr != null) {
    132                        uri = attr.getValue();
    133                        break;
    134                    }
    135                }
    136                aNode = aNode.getParentNode();
    137            }
    138        }
    139        return "".equals(uri) ? null : uri;
    140    }
    141
    142    /**
    143     * Gets any prefix from the specified node.
    144     *
    145     * @param node the node to check
    146     * @return String xml prefix
    147     */
    148    public static String getPrefix(final Node node) {
    149        final String prefix = node.getPrefix();
    150        if (prefix != null) {
    151            return prefix;
    152        }
    153        final String name = node.getNodeName();
    154        final int index = name.lastIndexOf(':');
    155        return index < 0 ? null : name.substring(0, index);
    156    }
    157
    158    /**
    159     * Test a Node.
    160     *
    161     * @param node to test
    162     * @param test to execute
    163     * @return true if node passes test
    164     */
    165    public static boolean testNode(final Node node, final NodeTest test) {
    166        if (test == null) {
    167            return true;
    168        }
    169        if (test instanceof NodeNameTest) {
    170            if (node.getNodeType() != Node.ELEMENT_NODE) {
    171                return false;
    172            }
    173            final NodeNameTest nodeNameTest = (NodeNameTest) test;
    174            final QName testName = nodeNameTest.getNodeName();
    175            final String namespaceURI = nodeNameTest.getNamespaceURI();
    176            final boolean wildcard = nodeNameTest.isWildcard();
    177            final String testPrefix = testName.getPrefix();
    178            if (wildcard && testPrefix == null) {
    179                return true;
    180            }
    181            if (wildcard || testName.getName().equals(getLocalName(node))) {
    182                final String nodeNS = getNamespaceURI(node);
    183                return equalStrings(namespaceURI, nodeNS) || nodeNS == null && equalStrings(testPrefix, getPrefix(node));
    184            }
    185            return false;
    186        }
    187        if (test instanceof NodeTypeTest) {
    188            final int nodeType = node.getNodeType();
    189            switch (((NodeTypeTest) test).getNodeType()) {
    190            case Compiler.NODE_TYPE_NODE:
    191                return true;
    192            case Compiler.NODE_TYPE_TEXT:
    193                return nodeType == Node.CDATA_SECTION_NODE || nodeType == Node.TEXT_NODE;
    194            case Compiler.NODE_TYPE_COMMENT:
    195                return nodeType == Node.COMMENT_NODE;
    196            case Compiler.NODE_TYPE_PI:
    197                return nodeType == Node.PROCESSING_INSTRUCTION_NODE;
    198            default:
    199                return false;
    200            }
    201        }
    202        if (test instanceof ProcessingInstructionTest && node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) {
    203            final String testPI = ((ProcessingInstructionTest) test).getTarget();
    204            final String nodePI = ((ProcessingInstruction) node).getTarget();
    205            return testPI.equals(nodePI);
    206        }
    207        return false;
    208    }
    209
    210    /**
    211     * A DOM node supporting {@link #getImmediateNode()}.
    212     */
    213    private final Node node;
    214
    215    /**
    216     * Supports {@link #getDefaultNamespaceURI()}.
    217     */
    218    private Map<String, String> namespaces;
    219
    220    /**
    221     * Supports {@link #getNamespaceURI(String)}.
    222     */
    223    private String defaultNamespace;
    224
    225    /**
    226     * Optional ID.
    227     */
    228    private final String id;
    229
    230    /**
    231     * Supports {@link #getNamespaceResolver()}.
    232     */
    233    private NamespaceResolver localNamespaceResolver;
    234
    235    /**
    236     * Constructs a new DOMNodePointer.
    237     *
    238     * @param node   A node.
    239     * @param locale Locale.
    240     */
    241    public DOMNodePointer(final Node node, final Locale locale) {
    242        this(node, locale, null);
    243    }
    244
    245    /**
    246     * Constructs a new DOMNodePointer.
    247     *
    248     * @param node   A node.
    249     * @param locale Locale.
    250     * @param id     String ID.
    251     */
    252    public DOMNodePointer(final Node node, final Locale locale, final String id) {
    253        super(null, locale);
    254        this.node = node;
    255        this.id = id;
    256    }
    257
    258    /**
    259     * Constructs a new DOMNodePointer.
    260     *
    261     * @param parent pointer
    262     * @param node   pointed
    263     */
    264    public DOMNodePointer(final NodePointer parent, final Node node) {
    265        super(parent);
    266        this.node = node;
    267        this.id = null;
    268    }
    269
    270    @Override
    271    public String asPath() {
    272        if (id != null) {
    273            return "id('" + escape(id) + "')";
    274        }
    275        final StringBuilder buffer = new StringBuilder();
    276        if (parent != null) {
    277            buffer.append(parent.asPath());
    278        }
    279        switch (node.getNodeType()) {
    280        case Node.ELEMENT_NODE:
    281            // If the parent pointer is not a DOMNodePointer, it is
    282            // the parent's responsibility to produce the node test part
    283            // of the path
    284            if (parent instanceof DOMNodePointer) {
    285                if (buffer.length() == 0 || buffer.charAt(buffer.length() - 1) != '/') {
    286                    buffer.append('/');
    287                }
    288                final String ln = getLocalName(node);
    289                final String nsURI = getNamespaceURI();
    290                if (nsURI == null) {
    291                    buffer.append(ln);
    292                    buffer.append('[');
    293                    buffer.append(getRelativePositionByQName()).append(']');
    294                } else {
    295                    final String prefix = getNamespaceResolver().getPrefix(nsURI);
    296                    if (prefix != null) {
    297                        buffer.append(prefix);
    298                        buffer.append(':');
    299                        buffer.append(ln);
    300                        buffer.append('[');
    301                        buffer.append(getRelativePositionByQName());
    302                    } else {
    303                        buffer.append("node()");
    304                        buffer.append('[');
    305                        buffer.append(getRelativePositionOfElement());
    306                    }
    307                    buffer.append(']');
    308                }
    309            }
    310            break;
    311        case Node.TEXT_NODE:
    312        case Node.CDATA_SECTION_NODE:
    313            buffer.append("/text()");
    314            buffer.append('[');
    315            buffer.append(getRelativePositionOfTextNode()).append(']');
    316            break;
    317        case Node.PROCESSING_INSTRUCTION_NODE:
    318            buffer.append("/processing-instruction(\'");
    319            buffer.append(((ProcessingInstruction) node).getTarget()).append("')");
    320            buffer.append('[');
    321            buffer.append(getRelativePositionOfPI()).append(']');
    322            break;
    323        case Node.DOCUMENT_NODE:
    324            // That'll be empty
    325            break;
    326        default:
    327            break;
    328        }
    329        return buffer.toString();
    330    }
    331
    332    @Override
    333    public NodeIterator attributeIterator(final QName qName) {
    334        return new DOMAttributeIterator(this, qName);
    335    }
    336
    337    @Override
    338    public NodeIterator childIterator(final NodeTest test, final boolean reverse, final NodePointer startWith) {
    339        return new DOMNodeIterator(this, test, reverse, startWith);
    340    }
    341
    342    @Override
    343    public int compareChildNodePointers(final NodePointer pointer1, final NodePointer pointer2) {
    344        final Node node1 = (Node) pointer1.getBaseValue();
    345        final Node node2 = (Node) pointer2.getBaseValue();
    346        if (node1 == node2) {
    347            return 0;
    348        }
    349        final int t1 = node1.getNodeType();
    350        final int t2 = node2.getNodeType();
    351        if (t1 == Node.ATTRIBUTE_NODE && t2 != Node.ATTRIBUTE_NODE) {
    352            return -1;
    353        }
    354        if (t1 != Node.ATTRIBUTE_NODE && t2 == Node.ATTRIBUTE_NODE) {
    355            return 1;
    356        }
    357        if (t1 == Node.ATTRIBUTE_NODE && t2 == Node.ATTRIBUTE_NODE) {
    358            final NamedNodeMap map = ((Node) getNode()).getAttributes();
    359            final int length = map.getLength();
    360            for (int i = 0; i < length; i++) {
    361                final Node n = map.item(i);
    362                if (n == node1) {
    363                    return -1;
    364                }
    365                if (n == node2) {
    366                    return 1;
    367                }
    368            }
    369            return 0; // Should not happen
    370        }
    371        Node current = node.getFirstChild();
    372        while (current != null) {
    373            if (current == node1) {
    374                return -1;
    375            }
    376            if (current == node2) {
    377                return 1;
    378            }
    379            current = current.getNextSibling();
    380        }
    381        return 0;
    382    }
    383
    384    @Override
    385    public NodePointer createAttribute(final JXPathContext context, final QName qName) {
    386        if (!(node instanceof Element)) {
    387            return super.createAttribute(context, qName);
    388        }
    389        final Element element = (Element) node;
    390        final String prefix = qName.getPrefix();
    391        if (prefix != null) {
    392            String ns = null;
    393            final NamespaceResolver nsr = getNamespaceResolver();
    394            if (nsr != null) {
    395                ns = nsr.getNamespaceURI(prefix);
    396            }
    397            if (ns == null) {
    398                throw new JXPathException("Unknown namespace prefix: " + prefix);
    399            }
    400            element.setAttributeNS(ns, qName.toString(), "");
    401        } else if (!element.hasAttribute(qName.getName())) {
    402            element.setAttribute(qName.getName(), "");
    403        }
    404        final NodeIterator it = attributeIterator(qName);
    405        it.setPosition(1);
    406        return it.getNodePointer();
    407    }
    408
    409    @Override
    410    public NodePointer createChild(final JXPathContext context, final QName qName, int index) {
    411        if (index == WHOLE_COLLECTION) {
    412            index = 0;
    413        }
    414        final boolean success = getAbstractFactory(context).createObject(context, this, node, qName.toString(), index);
    415        if (success) {
    416            NodeTest nodeTest;
    417            final String prefix = qName.getPrefix();
    418            final String namespaceURI = prefix == null ? null : context.getNamespaceURI(prefix);
    419            nodeTest = new NodeNameTest(qName, namespaceURI);
    420            final NodeIterator it = childIterator(nodeTest, false, null);
    421            if (it != null && it.setPosition(index + 1)) {
    422                return it.getNodePointer();
    423            }
    424        }
    425        throw new JXPathAbstractFactoryException("Factory could not create a child node for path: " + asPath() + "/" + qName + "[" + (index + 1) + "]");
    426    }
    427
    428    @Override
    429    public NodePointer createChild(final JXPathContext context, final QName qName, final int index, final Object value) {
    430        final NodePointer ptr = createChild(context, qName, index);
    431        ptr.setValue(value);
    432        return ptr;
    433    }
    434
    435    @Override
    436    public boolean equals(final Object object) {
    437        return object == this || object instanceof DOMNodePointer && node == ((DOMNodePointer) object).node;
    438    }
    439
    440    @Override
    441    public Object getBaseValue() {
    442        return node;
    443    }
    444
    445    @Override
    446    public String getDefaultNamespaceURI() {
    447        if (defaultNamespace == null) {
    448            Node aNode = node;
    449            if (aNode instanceof Document) {
    450                aNode = ((Document) aNode).getDocumentElement();
    451            }
    452            while (aNode != null) {
    453                if (aNode.getNodeType() == Node.ELEMENT_NODE) {
    454                    final Attr attr = ((Element) aNode).getAttributeNode("xmlns");
    455                    if (attr != null) {
    456                        defaultNamespace = attr.getValue();
    457                        break;
    458                    }
    459                }
    460                aNode = aNode.getParentNode();
    461            }
    462        }
    463        if (defaultNamespace == null) {
    464            defaultNamespace = "";
    465        }
    466        // TBD: We are supposed to resolve relative URIs to absolute ones.
    467        return defaultNamespace.isEmpty() ? null : defaultNamespace;
    468    }
    469
    470    @Override
    471    public Object getImmediateNode() {
    472        return node;
    473    }
    474
    475    /**
    476     * Gets the language attribute for this node.
    477     *
    478     * @return String language name
    479     */
    480    protected String getLanguage() {
    481        return findEnclosingAttribute(node, "xml:lang");
    482    }
    483
    484    @Override
    485    public int getLength() {
    486        return 1;
    487    }
    488
    489    @Override
    490    public QName getName() {
    491        String ln = null;
    492        String ns = null;
    493        final int type = node.getNodeType();
    494        if (type == Node.ELEMENT_NODE) {
    495            ns = getPrefix(node);
    496            ln = getLocalName(node);
    497        } else if (type == Node.PROCESSING_INSTRUCTION_NODE) {
    498            ln = ((ProcessingInstruction) node).getTarget();
    499        }
    500        return new QName(ns, ln);
    501    }
    502
    503    @Override
    504    public synchronized NamespaceResolver getNamespaceResolver() {
    505        if (localNamespaceResolver == null) {
    506            localNamespaceResolver = new NamespaceResolver(super.getNamespaceResolver());
    507            localNamespaceResolver.setNamespaceContextPointer(this);
    508        }
    509        return localNamespaceResolver;
    510    }
    511
    512    @Override
    513    public String getNamespaceURI() {
    514        return getNamespaceURI(node);
    515    }
    516
    517    @Override
    518    public String getNamespaceURI(final String prefix) {
    519        if (prefix == null || prefix.isEmpty()) {
    520            return getDefaultNamespaceURI();
    521        }
    522        if (prefix.equals("xml")) {
    523            return XML_NAMESPACE_URI;
    524        }
    525        if (prefix.equals("xmlns")) {
    526            return XMLNS_NAMESPACE_URI;
    527        }
    528        String namespace = null;
    529        if (namespaces == null) {
    530            namespaces = new HashMap<>();
    531        } else {
    532            namespace = namespaces.get(prefix);
    533        }
    534        if (namespace == null) {
    535            final String qname = "xmlns:" + prefix;
    536            Node aNode = node;
    537            if (aNode instanceof Document) {
    538                aNode = ((Document) aNode).getDocumentElement();
    539            }
    540            while (aNode != null) {
    541                if (aNode.getNodeType() == Node.ELEMENT_NODE) {
    542                    final Attr attr = ((Element) aNode).getAttributeNode(qname);
    543                    if (attr != null) {
    544                        namespace = attr.getValue();
    545                        break;
    546                    }
    547                }
    548                aNode = aNode.getParentNode();
    549            }
    550            if (namespace == null || namespace.isEmpty()) {
    551                namespace = UNKNOWN_NAMESPACE;
    552            }
    553        }
    554        namespaces.put(prefix, namespace);
    555        if (namespace == UNKNOWN_NAMESPACE) {
    556            return null;
    557        }
    558        // TBD: We are supposed to resolve relative URIs to absolute ones.
    559        return namespace;
    560    }
    561
    562    /**
    563     * Locates a node by ID.
    564     *
    565     * @param context starting context
    566     * @param id      to find
    567     * @return Pointer
    568     */
    569    @Override
    570    public Pointer getPointerByID(final JXPathContext context, final String id) {
    571        final Document document = node.getNodeType() == Node.DOCUMENT_NODE ? (Document) node : node.getOwnerDocument();
    572        final Element element = document.getElementById(id);
    573        return element == null ? (Pointer) new NullPointer(getLocale(), id) : new DOMNodePointer(element, getLocale(), id);
    574    }
    575
    576    /**
    577     * Gets relative position of this among like-named siblings.
    578     *
    579     * @return 1..n
    580     */
    581    private int getRelativePositionByQName() {
    582        int count = 1;
    583        Node n = node.getPreviousSibling();
    584        while (n != null) {
    585            if (n.getNodeType() == Node.ELEMENT_NODE && matchesQName(n)) {
    586                count++;
    587            }
    588            n = n.getPreviousSibling();
    589        }
    590        return count;
    591    }
    592
    593    /**
    594     * Gets relative position of this among all siblings.
    595     *
    596     * @return 1..n
    597     */
    598    private int getRelativePositionOfElement() {
    599        int count = 1;
    600        Node n = node.getPreviousSibling();
    601        while (n != null) {
    602            if (n.getNodeType() == Node.ELEMENT_NODE) {
    603                count++;
    604            }
    605            n = n.getPreviousSibling();
    606        }
    607        return count;
    608    }
    609
    610    /**
    611     * Gets the relative position of this among same-target processing instruction siblings.
    612     *
    613     * @return 1..n
    614     */
    615    private int getRelativePositionOfPI() {
    616        int count = 1;
    617        final String target = ((ProcessingInstruction) node).getTarget();
    618        Node n = node.getPreviousSibling();
    619        while (n != null) {
    620            if (n.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE && ((ProcessingInstruction) n).getTarget().equals(target)) {
    621                count++;
    622            }
    623            n = n.getPreviousSibling();
    624        }
    625        return count;
    626    }
    627
    628    /**
    629     * Gets the relative position of this among sibling text nodes.
    630     *
    631     * @return 1..n
    632     */
    633    private int getRelativePositionOfTextNode() {
    634        int count = 1;
    635        Node n = node.getPreviousSibling();
    636        while (n != null) {
    637            if (n.getNodeType() == Node.TEXT_NODE || n.getNodeType() == Node.CDATA_SECTION_NODE) {
    638                count++;
    639            }
    640            n = n.getPreviousSibling();
    641        }
    642        return count;
    643    }
    644
    645    @Override
    646    public Object getValue() {
    647        if (node.getNodeType() == Node.COMMENT_NODE) {
    648            final String text = ((Comment) node).getData();
    649            return text == null ? "" : text.trim();
    650        }
    651        return stringValue(node);
    652    }
    653
    654    @Override
    655    public int hashCode() {
    656        return node.hashCode();
    657    }
    658
    659    @Override
    660    public boolean isActual() {
    661        return true;
    662    }
    663
    664    @Override
    665    public boolean isCollection() {
    666        return false;
    667    }
    668
    669    /**
    670     * Returns true if the xml:lang attribute for the current node or its parent has the specified prefix <em>lang</em>. If no node has this prefix, calls
    671     * {@code super.isLanguage(lang)}.
    672     *
    673     * @param lang ns to test
    674     * @return boolean
    675     */
    676    @Override
    677    public boolean isLanguage(final String lang) {
    678        final String current = getLanguage();
    679        return current == null ? super.isLanguage(lang) : current.toUpperCase(Locale.ENGLISH).startsWith(lang.toUpperCase(Locale.ENGLISH));
    680    }
    681
    682    @Override
    683    public boolean isLeaf() {
    684        return !node.hasChildNodes();
    685    }
    686
    687    private boolean matchesQName(final Node n) {
    688        if (getNamespaceURI() != null) {
    689            return equalStrings(getNamespaceURI(n), getNamespaceURI()) && equalStrings(node.getLocalName(), n.getLocalName());
    690        }
    691        return equalStrings(node.getNodeName(), n.getNodeName());
    692    }
    693
    694    @Override
    695    public NodeIterator namespaceIterator() {
    696        return new DOMNamespaceIterator(this);
    697    }
    698
    699    @Override
    700    public NodePointer namespacePointer(final String prefix) {
    701        return new NamespacePointer(this, prefix);
    702    }
    703
    704    @Override
    705    public void remove() {
    706        final Node parent = node.getParentNode();
    707        if (parent == null) {
    708            throw new JXPathException("Cannot remove root DOM node");
    709        }
    710        parent.removeChild(node);
    711    }
    712
    713    /**
    714     * Sets contents of the node to the specified value. If the value is a String, the contents of the node are replaced with this text. If the value is an
    715     * Element or Document, the children of the node are replaced with the children of the passed node.
    716     *
    717     * @param value to set
    718     */
    719    @Override
    720    public void setValue(final Object value) {
    721        if (node.getNodeType() == Node.TEXT_NODE || node.getNodeType() == Node.CDATA_SECTION_NODE) {
    722            final String string = (String) TypeUtils.convert(value, String.class);
    723            if (string != null && !string.isEmpty()) {
    724                node.setNodeValue(string);
    725            } else {
    726                node.getParentNode().removeChild(node);
    727            }
    728        } else {
    729            NodeList children = node.getChildNodes();
    730            final int count = children.getLength();
    731            for (int i = count; --i >= 0;) {
    732                final Node child = children.item(i);
    733                node.removeChild(child);
    734            }
    735            if (value instanceof Node) {
    736                final Node valueNode = (Node) value;
    737                if (valueNode instanceof Element || valueNode instanceof Document) {
    738                    children = valueNode.getChildNodes();
    739                    for (int i = 0; i < children.getLength(); i++) {
    740                        final Node child = children.item(i);
    741                        node.appendChild(child.cloneNode(true));
    742                    }
    743                } else {
    744                    node.appendChild(valueNode.cloneNode(true));
    745                }
    746            } else {
    747                final String string = (String) TypeUtils.convert(value, String.class);
    748                if (string != null && !string.isEmpty()) {
    749                    final Node textNode = node.getOwnerDocument().createTextNode(string);
    750                    node.appendChild(textNode);
    751                }
    752            }
    753        }
    754    }
    755
    756    /**
    757     * Gets the string value of the specified node.
    758     *
    759     * @param node Node to check
    760     * @return String
    761     */
    762    private String stringValue(final Node node) {
    763        final int nodeType = node.getNodeType();
    764        if (nodeType == Node.COMMENT_NODE) {
    765            return "";
    766        }
    767        final boolean trim = !"preserve".equals(findEnclosingAttribute(node, "xml:space"));
    768        if (nodeType == Node.TEXT_NODE || nodeType == Node.CDATA_SECTION_NODE) {
    769            final String text = node.getNodeValue();
    770            return text == null ? "" : trim ? text.trim() : text;
    771        }
    772        if (nodeType == Node.PROCESSING_INSTRUCTION_NODE) {
    773            final String text = ((ProcessingInstruction) node).getData();
    774            return text == null ? "" : trim ? text.trim() : text;
    775        }
    776        final NodeList list = node.getChildNodes();
    777        final StringBuilder buf = new StringBuilder();
    778        for (int i = 0; i < list.getLength(); i++) {
    779            final Node child = list.item(i);
    780            buf.append(stringValue(child));
    781        }
    782        return buf.toString();
    783    }
    784
    785    @Override
    786    public boolean testNode(final NodeTest test) {
    787        return testNode(node, test);
    788    }
    789}