/*
 * Copyright 2015 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * 
 *      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.eclipse.webdav.dom;

import java.util.Enumeration;
import java.util.Vector;
import org.eclipse.webdav.Policy;
import org.eclipse.webdav.internal.utils.URLDecoder;
import org.eclipse.webdav.internal.utils.URLEncoder;
import org.w3c.dom.*;
import org.w3c.dom.CharacterData;

/**
 * Abstract superclass of all element editors.  An element editor is a
 * convenience for manipulating DOM elements.  Specific subclasses declared
 * in this package correspond to the major DOM element types for the WebDAV
 * protocol; e.g., the class <code>ResponseBody</code> corresponds to
 * &lt;DAV:response&gt;.
 * <p>
 * Element editors contain no state of their own; all they they hang onto
 * is a single DOM <code>Element</code>; all information is stored in the DOM
 * itself.  Thus element editors are really just lightweight, throw-away
 * wrappers.</p>
 * <p>
 * Element editors assume that they are working with elements that are "up to
 * spec".  When an element editor encounters an element that is not how it
 * should be, it throws a <code>MalformedElementException</code> (a checked
 * exception).  The editor does not provide any further diagnosis of what's
 * wrong.  Clients catching this exception will generally conclude that they're
 * talking to a non-compliant (or malfunctioning) WebDAV server.</p>
 * <p>
 * Fully-capable element editors can be written using the DOM API.  No access
 * is required to any concrete implementation of the DOM.  The element editors
 * in this package all have this property.</p>
 * <p>
 * This class also provides rudimentary XML namespace support and element
 * navigation and manipulation methods that are useful when writing element
 * editors.</p>
 * <p>
 * <b>Note:</b> This class/interface is part of an interim API that is still under 
 * development and expected to change significantly before reaching stability. 
 * It is being made available at this early stage to solicit feedback from pioneering 
 * adopters on the understanding that any code that uses this API will almost 
 * certainly be broken (repeatedly) as the API evolves.
 * </p>
 */
public abstract class ElementEditor {

    // The root element upon which this editor is based.
    // This field is never <code>null</code>.
    // However, it cannot be assumed that this element conforms
    // to the appropriate specification (the DOM can be changed
    // from underneath its editor).
    protected Element root;

    // The official URI for WebDAV namespace.
    protected static final String DAV_NS = "DAV:"; //$NON-NLS-1$

    // The official prefix for XML namespace declarations,
    // namely "xmlns" (note: no colon).
    protected static final String XML_PREFIX = "xmlns"; //$NON-NLS-1$

    // The official XML namespace prefix.
    protected static final String XML_NS_PREFIX = "xml"; //$NON-NLS-1$

    // The official XML namespace name.
    protected static final String XML_NS_NAME = "http://www.w3.org/XML/1998/namespace"; //$NON-NLS-1$

    /**
     * Constructs a new element editor for the given element. The element
     * must not be <code>null</code>.
     *
     * @param root the element to be edited
     * @throws        MalformedElementException if the element is not null
     */
    protected ElementEditor(Element root) throws MalformedElementException {
        Assert.isNotNull(root);
        this.root = root;
    }

    /**
     * Constructs a new element editor for the given element. The element
     * must not be <code>null</code>.
     *
     * @param root the element to be edited
     * @throws        MalformedElementException if the element is not null
     */
    protected ElementEditor(Element root, String expectedType) throws MalformedElementException {
        Assert.isNotNull(root);
        Assert.isNotNull(expectedType);
        this.root = root;
        ensureDAVElement(root, expectedType, Policy.bind("ensure.expectingAnElmt", expectedType)); //$NON-NLS-1$
    }

    /**
     * <p>Creates a WebDAV element with the given name and adds it as a
     * child of the given parent. Returns the child element.
     * <p>Children are positioned in the order specified by the given names.
     * If children with the same name as the child already exist, the child
     * is inserted after the rightmost child.  If firstToLast is true, the
     * search for the child's position starts at the parent's first child,
     * otherwise, the search starts at the parent's last child.
     * <p>The parent must not be <code>null</code> and must be a WebDAV
     * element.  The child's name must not be <code>null</code>.  The
     * parent's valid child names must not be <code>null</code>, and must
     * contain the name of the child.
     *
     * @param parent      the parent to which the child is added
     * @param name        the name of the child which is created and added
     *                    to the parent
     * @param names       the ordered collection of valid child names for
     *                    the parent
     * @param firstToLast a boolean specifying the direction to search for
     *                    the child's position among the parent's children
     * @return            the child element that is created
     */
    public static Element addChild(Element parent, String name, String[] names, boolean firstToLast) {
        Assert.isTrue(isDAVElement(parent));
        Assert.isNotNull(name);
        Assert.isNotNull(names);
        String nsPrefix = getNSPrefix(parent);
        String tagName = nsPrefix == null ? name : nsPrefix + ":" + name; //$NON-NLS-1$
        Element child = parent.getOwnerDocument().createElement(tagName);
        addChild(parent, child, names, firstToLast);
        return child;
    }

    /**
     * <p>Creates a WebDAV element with the given name and adds it as a
     * child of the given parent. In addition, a text node created from the
     * given data is created and appended to the child.  Returns the child
     * element.
     * <p>Children are positioned in the order specified by the given names.
     * If children with the same name as the child already exist, the child
     * is inserted after the rightmost child.  If firstToLast is true, the
     * search for the child's position starts at the parent's first child,
     * otherwise, the search starts at the parent's last child.
     * <p>The parent must not be <code>null</code> and must be a WebDAV
     * element.  The child's name and data must not be <code>null</code>.
     * The parent's valid child names must not be <code>null</code>, and
     * must contain the name of the child.
     *
     * @param parent      the parent to which the child is added
     * @param name        the name of the child which is created and added
     *                    to the parent
     * @param data        the data of the text node which is created and
     *                    added to the child
     * @param names       the ordered collection of valid child names for
     *                    the parent
     * @param firstToLast a boolean specifying the direction to search for
     *                    the child's position among the parent's children
     * @return            the child element that is created
     */
    public static Element addChild(Element parent, String name, String data, String[] names, boolean firstToLast) {
        Assert.isTrue(isDAVElement(parent));
        Assert.isNotNull(name);
        Assert.isNotNull(data);
        Assert.isNotNull(names);
        Element child = addChild(parent, name, names, firstToLast);
        child.appendChild(child.getOwnerDocument().createTextNode(data));
        return child;
    }

    /**
     * Adds the given child element as a child of the given parent.
     * <p>
     * Children are positioned in the order specified by the given names.
     * If children with the same name as the child already exist, the child
     * is inserted after the rightmost child.  If firstToLast is true, the
     * search for the child's position starts at the parent's first child,
     * otherwise, the search starts at the parent's last child.</p>
     * <p>
     * The parent must not be <code>null</code> and must be a WebDAV
     * element. The child must not be null and its namespace prefix must
     * resolve to the WebDAV namespace URL in the parent. The parent's valid
     * child names must not be <code>null</code>, and must contain the name
     * of the child.</p>
     *
     * @param parent      the parent to which the child is added
     * @param child       the child which is added to the parent
     * @param names       the ordered collection of valid child names for
     *                    the parent
     * @param firstToLast a boolean specifying the direction to search for
     *                    the child's position among the parent's children
     */
    public static void addChild(Element parent, Element child, String[] names, boolean firstToLast) {

        Assert.isTrue(isDAVElement(parent));
        Assert.isNotNull(child);
        Assert.isTrue(DAV_NS.equals(resolve(getNSPrefix(child), parent)));
        Assert.isNotNull(names);

        boolean found = false;
        String name = getNSLocalName(child);
        for (int i = 0; !found && i < names.length; ++i)
            found = names[i].equals(name);
        Assert.isTrue(found);

        Node sibling = getChild(parent, name, names, firstToLast);

        if (firstToLast) {
            if (sibling == null)
                parent.appendChild(child);
            else {
                Node lastTwin = null;
                while (isDAVElement(sibling, name)) {
                    lastTwin = sibling;
                    sibling = getTwin((Element) sibling, firstToLast);
                }
                if (lastTwin == null)
                    parent.insertBefore(child, sibling);
                else {
                    Node refChild = lastTwin.getNextSibling();
                    if (refChild == null)
                        parent.appendChild(child);
                    else
                        parent.insertBefore(child, refChild);
                }
            }
        } else {
            Node refChild = null;
            if (sibling == null) {
                refChild = parent.getFirstChild();
            } else {
                refChild = sibling.getNextSibling();
            }
            if (refChild == null) {
                parent.appendChild(child);
            } else {
                parent.insertBefore(child, refChild);
            }
        }
    }

    /**
     * Creates a WebDAV element with the given name and appends it as a
     * child of the given parent. Returns the child element. The parent must
     * not be <code>null</code> and must be a WebDAV element. The name of
     * the child must not be <code>null</code>.
     *
     * @param parent the parent element to which the child is added
     * @param name   the name of the child element that is created and added
     *               as a child of the parent
     * @return       the child element that is created
     */
    public static Element appendChild(Element parent, String name) {
        Assert.isTrue(isDAVElement(parent));
        Assert.isNotNull(name);
        String nsPrefix = getNSPrefix(parent);
        String tagName = nsPrefix == null ? name : nsPrefix + ":" + name; //$NON-NLS-1$
        Element child = parent.getOwnerDocument().createElement(tagName);
        parent.appendChild(child);
        return child;
    }

    /**
     * Creates a WebDAV element with the given name and appends it as a
     * child of the given parent. In addition, a text node created from the
     * given data is created and appended to the child. Returns the child
     * element. The parent must not be <code>null</code> and must be a
     * WebDAV element. The name of the child must not be <code>null</code>.
     * The data must not be <code>null</code>.
     *
     * @param parent the parent element to which the child is added
     * @param name   the name of the child element that is created and added
     *               as a child of the parent
     * @param data   the data of the text node which is created and added to
     *               the child
     * @return       the child element that is created
     */
    public static Element appendChild(Element parent, String name, String data) {
        Assert.isTrue(isDAVElement(parent));
        Assert.isNotNull(name);
        Assert.isNotNull(data);
        Element child = appendChild(parent, name);
        child.appendChild(child.getOwnerDocument().createTextNode(data));
        return child;
    }

    /**
     * Returns a clone of the given node.  The given document becomes the
     * owner document of the clone.
     *
     * @param document the owner document of the clone
     * @param node     the node to clone
     * @return         a clone of the given node
     */
    public static Node cloneNode(Document document, Node node) {

        Node nodeClone = null;

        switch (node.getNodeType()) {
            case Node.ELEMENT_NODE :
                {
                    nodeClone = document.createElement(((Element) node).getTagName());
                    NamedNodeMap namedNodeMap = node.getAttributes();
                    for (int i = 0; i < namedNodeMap.getLength(); ++i) {
                        Attr attr = (Attr) namedNodeMap.item(i);
                        Attr attrClone = document.createAttribute(attr.getName());
                        attrClone.setValue(attr.getValue());
                        ((Element) nodeClone).setAttributeNode(attrClone);
                    }
                }
                break;
            case Node.TEXT_NODE :
                nodeClone = document.createTextNode(((CharacterData) node).getData());
                break;
            case Node.CDATA_SECTION_NODE :
                nodeClone = document.createCDATASection(((CharacterData) node).getData());
                break;
            case Node.ENTITY_REFERENCE_NODE :
                nodeClone = document.createEntityReference(node.getNodeName());
                break;
            case Node.PROCESSING_INSTRUCTION_NODE :
                nodeClone = document.createProcessingInstruction(((ProcessingInstruction) node).getTarget(), ((ProcessingInstruction) node).getData());
                break;
            case Node.COMMENT_NODE :
                nodeClone = document.createComment(((CharacterData) node).getData());
                break;
            case Node.DOCUMENT_FRAGMENT_NODE :
                nodeClone = document.createDocumentFragment();
                break;
            case Node.DOCUMENT_NODE :
            case Node.DOCUMENT_TYPE_NODE :
            case Node.NOTATION_NODE :
            case Node.ATTRIBUTE_NODE :
            case Node.ENTITY_NODE :
                Assert.isTrue(false, Policy.bind("assert.notSupported")); //$NON-NLS-1$
                break;
            default :
                Assert.isTrue(false, Policy.bind("assert.unknownNodeType")); //$NON-NLS-1$
        }

        return nodeClone;
    }

    /**
     * Creates a WebDAV element with the given name and adds it as the root
     * of the given document. In addition, the WebDAV namespace is declared
     * on the new element. Returns the new element. The document must not be
     * <code>null</code> and must not already have a root element. The name
     * of the element to be created must not be <code>null</code>.
     *
     * @param document the document in which the new element is rooted
     * @param name     the name of the element to be created
     * @return         the rooted element
     */
    public static Element create(Document document, String name) {
        Assert.isNotNull(document);
        Assert.isTrue(document.getDocumentElement() == null);
        Assert.isNotNull(name);
        Element element = document.createElement(name);
        declareNS(element, null, DAV_NS);
        document.appendChild(element);
        return element;
    }

    /**
     * Adds a namespace declaration to the given element. If only the prefix
     * is <code>null</code>, a default namespace is declared. If the prefix
     * and the namespaceUrl are <code>null</code> the default namespace is
     * removed. The element must not be <code>null</code>. If the
     * namespaceUrl is <code>null</code>, the <code>prefix</code> must also
     * be <code>null</code>.
     *
     * @param element      the element to receive the namespace attribute
     * @param prefix       the prefix to be used (without a colon); for
     *                     example, "D", or <code>null</code>
     * @param namespaceUrl the URL of the namespace; for example, "DAV:", or
     *                     <code>null</code>
     */
    public static void declareNS(Element element, String prefix, String namespaceUrl) {
        Assert.isNotNull(element);
        Assert.isTrue(namespaceUrl != null || prefix == null && namespaceUrl == null);
        String name = XML_PREFIX + (prefix == null ? "" : ":" + prefix); //$NON-NLS-1$ //$NON-NLS-2$
        String value = namespaceUrl == null ? "" : namespaceUrl; //$NON-NLS-1$
        element.setAttribute(name, value);
    }

    /**
     * Decodes the given href from a form that is safe for transport.
     *
     * @param href the href to be decoded
     */
    public static String decodeHref(String href) {
        return URLDecoder.decode(href);
    }

    /**
     * Encodes the given href to a form that is safe for transport.
     *
     * @param href the href to be encoded
     */
    public static String encodeHref(String href) {
        return URLEncoder.encode(href);
    }

    /**
     * Ensures that the given value is <code>true</code>.
     *
     * @param message the message for the exception that is thrown if the
     *                given value is <code>false</code>
     * @param value   a boolean indicating whether or not a malformed
     *                element exception should be thrown
     * @throws        MalformedElementException with the given message if
     *                the given value is <code>false</code>
     * @deprecated
     */
    protected static void ensure(String message, boolean value) throws MalformedElementException {
        if (!value)
            throw new MalformedElementException(message);
    }

    /**
     * Ensures that the given value is <code>true</code>.
     *
     * @param message the message for the exception that is thrown if the
     *                given value is <code>false</code>
     * @param value   a boolean indicating whether or not a malformed
     *                element exception should be thrown
     * @throws        MalformedElementException with the given message if
     *                the given value is <code>false</code>
     */
    protected static void ensure(boolean value, String message) throws MalformedElementException {
        if (!value)
            throw new MalformedElementException(message);
    }

    /**
     * Ensures that the given node is a WebDAV element with the given name,
     * returning it as an <code>Element</code> if it is.
     *
     * @param message the message for the exception that is thrown if the
     *                node is not a WebDAV element with the given name
     * @param node    the node, or <code>null</code>
     * @param name    the expected local name of the WebDAV element
     * @returns       the node as an <code>Element</code>
     * @throws        MalformedElementException with the given message if it
     *                is not a WebDAV element with the given name
     * @deprecated
     */
    protected static Element ensureDAVElement(String message, Node node, String name) throws MalformedElementException {
        Assert.isNotNull(name);
        if (node == null || node.getNodeType() != Node.ELEMENT_NODE)
            throw new MalformedElementException(message);
        Element element = (Element) node;
        if (!name.equals(getNSLocalName(element)) || !DAV_NS.equals(getNSName(element)))
            throw new MalformedElementException(message);
        return element;
    }

    /**
     * Ensures that the given node is a WebDAV element with the given name.
     *
     * @param node    the node, or <code>null</code>
     * @param name    the expected local name of the WebDAV element
     * @param message the message for the exception that is thrown if the
     *                node is not a WebDAV element with the given name
     * @throws        MalformedElementException with the given message if it
     *                is not a WebDAV element with the given name
     */
    protected static void ensureDAVElement(Node node, String name, String message) throws MalformedElementException {
        Assert.isNotNull(name);
        if (node == null || node.getNodeType() != Node.ELEMENT_NODE)
            throw new MalformedElementException(message);
        Element element = (Element) node;
        if (!name.equals(getNSLocalName(element)) || !DAV_NS.equals(getNSName(element)))
            throw new MalformedElementException(message);
    }

    /**
     * Ensures that the given object is not <code>null</code>.
     *
     * @param message the message for the exception that is thrown if the
     *                object is <code>null</code>
     * @param object  the object, or <code>null</code>
     * @throws        MalformedElementException if the given object is
     *                <code>null</code>
     */
    protected static void ensureNotNull(String message, Object object) throws MalformedElementException {
        if (object == null)
            throw new MalformedElementException(message);
    }

    /**
     * Ensures that the given object is <code>null</code>.
     *
     * @param message the message for the exception that is thrown if the
     *                object is not <code>null</code>
     * @param object  the object, or <code>null</code>
     * @throws        MalformedElementException if the given object is not
     *                <code>null</code>
     */
    protected static void ensureNull(String message, Object object) throws MalformedElementException {
        if (object != null)
            throw new MalformedElementException(message);
    }

    /**
     * Ensures that the given node is a text node, returning it as a
     * <code>Text</code> node if it is.
     *
     * @param message the message for the exception that is thrown if the
     *                node is not a <code>Text</code> node
     * @param node    the node, or <code>null</code>
     * @returns       the node as a <code>Text</code> node
     * @exception     MalformedElementException if the node is
     *                <code>null</code> or not a text node
     */
    protected static Text ensureText(String message, Node node) throws MalformedElementException {
        if (node == null || node.getNodeType() != Node.TEXT_NODE)
            throw new MalformedElementException(message);
        return (Text) node;
    }

    /**
     * Clones the given element, and its subtrees, and sets it as the root
     * of the given document. Returns the cloned element. The document must
     * not have a root and must not be <code>null</code>. The element must
     * not be <code>null</code>.
     *
     * @param document the document that will become the owner and parent
     *                 of the cloned element
     * @param element  the element to be cloned
     * @returns        a clone of the element
     * @throws         MalformedElementException if an element exists in the
     *                 subtree to be cloned whose namespace can't be
     *                 resolved
     */
    public static Element extractElement(Document document, Element element) throws MalformedElementException {
        Assert.isNotNull(document);
        Assert.isTrue(document.getDocumentElement() == null);
        Assert.isNotNull(element);
        return (Element) extractNode(document, element);
    }

    /**
     * Clones the given node, and its subtrees, and sets it as the root of
     * the given parent node. Returns the cloned node. The parent node and
     * the node to be cloned must not be <code>null</code>.
     *
     * @param parent  the node that will become the parent of the cloned
     *                node
     * @param node the node to be cloned
     * @returns       a clone of the element
     * @throws        MalformedElementException if an element exists in the
     *                subtree to be cloned whose namespace can't be resolved
     */
    public static Node extractNode(Node parent, Node node) throws MalformedElementException {

        // Get a handle to the root of the parent node tree.
        Document document;
        if (parent.getNodeType() == Node.DOCUMENT_NODE)
            document = (Document) parent;
        else
            document = parent.getOwnerDocument();

        // Create a clone of the node owned by the document, and add to the parent.
        Node nodeClone = cloneNode(document, node);
        parent.appendChild(nodeClone);

        // If the node is an Element
        if (node.getNodeType() == Node.ELEMENT_NODE) {

            // Figure out its namespace information.
            String nsPrefix = getNSPrefix((Element) node);
            String nsName = getNSName((Element) node);

            // Is this namespace already defined on the clone?  If not declare it.
            String nsNameClone = resolve(nsPrefix, (Element) nodeClone);
            if (nsName != nsNameClone && (nsName == null || !nsName.equals(nsNameClone)))
                declareNS((Element) nodeClone, nsPrefix, nsName);

            // Do the same namespace fix-up for each of the node's attributes.
            NamedNodeMap nodeMap = nodeClone.getAttributes();
            for (int i = 0; i < nodeMap.getLength(); ++i) {
                Attr attr = (Attr) nodeMap.item(i);
                nsPrefix = getNSPrefix(attr.getName());
                if (nsPrefix != null && !nsPrefix.equals(XML_PREFIX)) {
                    nsName = resolve(nsPrefix, (Element) node);
                    nsNameClone = resolve(nsPrefix, (Element) nodeClone);
                    if (nsName != nsNameClone && (nsName == null || !nsName.equals(nsNameClone)))
                        declareNS((Element) nodeClone, nsPrefix, nsName);
                }
            }
        }

        // Recursively clone each of the node's children.
        Node child = node.getFirstChild();
        while (child != null) {
            extractNode(nodeClone, child);
            child = child.getNextSibling();
        }

        // Finished cloning this node.
        return nodeClone;
    }

    /**
     * Returns the first child of the given parent that is a WebDAV element
     * with one of the given names, or <code>null</code> if no such child
     * exists.
     * <p>
     * If firstToLast is true, the search for the child starts at the parent's
     * first child, otherwise, the search starts at the parent's last child.
     * The parent must not be <code>null</code> and must be a WebDAV element.
     * The names of children to search for must not be <code>null</code>.</p>
     *
     * @param parent      the parent of the child to search.
     * @param names       all possible names of the child to search for.
     * @param firstToLast a boolean specifying the direction to search for
     *                    the child among the parent's children.
     * @return            the specified child of the parent, or
     *                    <code>null<code> if no such child exists.
     */
    private static Element getChild(Element parent, String[] names, boolean firstToLast) {

        Assert.isTrue(isDAVElement(parent));
        Assert.isNotNull(names);

        // Get the first candidate.
        Node child = null;
        if (firstToLast)
            child = parent.getFirstChild();
        else
            child = parent.getLastChild();

        // While there are children left to consider.
        while (child != null) {

            // See if the child name matches any being sought.
            for (int i = 0; i < names.length; ++i)
                if (isDAVElement(child, names[i]))
                    return (Element) child;

            // Try the next child.
            if (firstToLast)
                child = child.getNextSibling();
            else
                child = child.getPreviousSibling();
        }

        // A matching child was not found.
        return null;
    }

    /**
     * <p>Returns the child WebDAV element of the given parent that is
     * nearest in position to a WebDAV element with the given name, or
     * <code>null</code> if no such child exists.
     * <p>Children are expected to be in the order specified by the given
     * names. If firstToLast is true, the search for the child starts at the
     * parent's first child, otherwise, the search starts at the parent's
     * last child.
     * <p>The parent must not be <code>null</code> and must be a WebDAV
     * element.  The name of the child to search for must not be
     * <code>null</code>. The parent's valid child names must not be
     * <code>null</code>, and must contain the name of the child being
     * searched for.
     * <p>The returned child is as follows:
     * <ul>
     * <li>Searching first to last</li>
     *   <ul>
     *   <li>returns <code>null</code> if an element with the given name
     *       should appear as the last child</li>
     *   <li>returns the first occurring child if an element with the given
     *       name is a child</li>
     *   <li>returns a child if an element with the given name would appear
     *       before it</li>
     *   </ul>
     * <li>Searching last to first</li>
     *   <ul>
     *   <li>returns <code>null</code> if an element with the given name
     *               would appear as the first child</li>
     *   <li>returns the last occurring child if an element with the given
     *       name is a child</li>
     *   <li>returns a child if an element with the given name would appear
     *       after it</li>
     *   </ul>
     * </ul>
     * @param parent      the parent of the child being searched for
     * @param name        the name of the child being searched for
     * @param names       the ordered collection of valid child names for
     *                    the parent
     * @param firstToLast a boolean specifying the direction to search for
     *                    the specified child among the parent's children
     * @return            a child of the parent, or <code>null</code>
     */
    public static Element getChild(Element parent, String name, String[] names, boolean firstToLast) {
        Assert.isNotNull(parent);
        Assert.isNotNull(name);
        Assert.isNotNull(names);

        boolean found = false;
        for (int i = 0; !found && i < names.length; ++i) {
            found = names[i].equals(name);
        }
        Assert.isTrue(found);

        int i;
        Node child = null;

        if (firstToLast) {
            i = 0;
            child = parent.getFirstChild();
        } else {
            i = names.length - 1;
            child = parent.getLastChild();
        }

        while (child != null && !names[i].equals(name)) {
            int mark = i;
            while (!isDAVElement(child, names[i]) && !names[i].equals(name)) {
                if (firstToLast) {
                    ++i;
                } else {
                    --i;
                }
            }

            if (!names[i].equals(name)) {
                if (firstToLast) {
                    child = child.getNextSibling();
                } else {
                    child = child.getPreviousSibling();
                }
            } else if (!isDAVElement(child, names[i])) {
                int pos = i;
                found = false;
                while (!found && (pos >= 0 && pos < names.length)) {
                    found = isDAVElement(child, names[pos]);
                    if (firstToLast) {
                        ++pos;
                    } else {
                        --pos;
                    }
                }
                if (!found) {
                    i = mark;
                    if (firstToLast) {
                        child = child.getNextSibling();
                    } else {
                        child = child.getPreviousSibling();
                    }
                }
            }
        }

        return (Element) child;
    }

    /**
     * Returns the first child element of the given parent element, or
     * <code>null</code> if no such child exists. If firstToLast is true,
     * the search for the child starts at the parent's first child,
     * otherwise, the search starts at the parent's last child. The parent
     * must not be <code>null</code>.
     *
     * @param parent      the parent of the child element to search for
     * @param firstToLast a boolean specifying the direction to search for
     *                    the child element among the parent's children
     * @return            the child element, or <code>null</code> if no such
     *                    child exists
     */
    public static Element getChildElement(Element parent, boolean firstToLast) {
        Assert.isNotNull(parent);
        Node child = null;
        if (firstToLast)
            child = parent.getFirstChild();
        else
            child = parent.getLastChild();
        while (child != null && !isElement(child)) {
            if (firstToLast)
                child = child.getNextSibling();
            else
                child = child.getPreviousSibling();
        }
        return (Element) child;
    }

    /**
     * Returns the data of the first child text node of the first WebDAV
     * child with the given name of the given parent, or the empty
     * <code>String</code> if no such child text node exists, or
     * <code>null</code> if no such child exists. If firstToLast is true, the
     * search for the child starts at the parent's first child, otherwise,
     * the search starts at the parent's last child. The parent must not be
     * <code>null</code> and must be a WebDAV element. The name of the child
     * must not be <code>null</code>.
     *
     * @param parent      the parent of the child with the child text node
     *                    to search for
     * @param name        the name of the child with the child text node to
     *                    search for
     * @param firstToLast a boolean specifying the direction to search for
     *                    the specified child among the parent's children
     * @return            the data of the child's child text node, or the
     *                    empty <code>String</code> if no such child exists,
     *                    or <code>null</code> if no such child exists
     */
    public static String getChildText(Element parent, String name, boolean firstToLast) {
        Assert.isTrue(isDAVElement(parent));
        Assert.isNotNull(name);
        Element child;
        if (firstToLast)
            child = getFirstChild(parent, name);
        else
            child = getLastChild(parent, name);
        if (child != null)
            return getText(child, firstToLast);
        return null;
    }

    /**
     * Returns the first WebDAV child of the given parent, or
     * <code>null</code> if no such child exists. The parent must not be
     * <code>null</code> and must be a WebDAV element.
     *
     * @param parent the parent of the child to search for
     * @return the first WebDAV child of the parent, or null
     */
    public static Element getDAVChild(Element parent) {
        Assert.isTrue(isDAVElement(parent));
        Node child = parent.getFirstChild();
        while (child != null && !isDAVElement(child))
            child = child.getNextSibling();
        return (Element) child;
    }

    /**
     * Returns the root element upon which this editor is based. It cannot
     * be assumed that this element necessarily conforms to the appropriate
     * specification.
     *
     * @return the root element upon which this editor is based
     */
    public Element getElement() {
        return root;
    }

    /**
     * Returns the first child of the given parent that is a WebDAV element
     * with one of the given names, or <code>null</code> if no such child
     * exists.
     * <p>
     * The search for the child starts at the parent's first child.
     * The parent must not be <code>null</code> and must be a WebDAV element.
     * The names of children to search for must not be <code>null</code>.</p>
     *
     * @param parent      the parent of the child to search.
     * @param names       all possible names of the child to search for.
     * @return            the specified child of the parent, or
     *                    <code>null<code> if no such child exists.
     */
    public static Element getFirstChild(Element parent, String[] names) {
        return getChild(parent, names, true);
    }

    /**
     * Returns the first child of the given parent that is a WebDAV element
     * with the given name, or <code>null</code> if no such child exists.
     * <p>
     * The search for the child starts at the parent's first child.
     * The parent must not be <code>null</code> and must be a DAV: namespace
     * element. The name of the child to search for must not be
     * <code>null</code>.
     *
     * @param parent      the parent of the child to search.
     * @param name        the name of the child to search for.
     * @return            the specified child of the parent, or <code>null
     *                    </code> if no such child exists.
     */
    public static Element getFirstChild(Element parent, String name) {
        Assert.isNotNull(name);
        return getChild(parent, new String[] {name}, true);
    }

    /**
     * Returns the data of the given parent's first text node, or the empty
     * <code>String</code> if no such text node exists.  The search for the
     * text node starts at the parent's first child. The parent must not be
     * <code>null</code>.
     *
     * @param parent      the parent of the text node to search for
     * @return            the data of the text node being searched for, or
     *                    the empty <code>String</code> if no such text node
     *                    exists
     */
    public static String getFirstText(Element parent) {
        Assert.isNotNull(parent);
        Node child = parent.getFirstChild();
        while (child != null && !isText(child))
            child = child.getNextSibling();
        if (child == null)
            return ""; //$NON-NLS-1$
        return ((Text) child).getData();
    }

    /**
     * Returns the last child of the given parent that is a WebDAV element
     * with the given name, or <code>null</code> if no such child exists.
     * <p>
     * The search starts at the parent's last child.
     * The parent must not be <code>null</code> and must be a DAV: namespace
     * element. The name of the child to search for must not be
     * <code>null</code>.
     *
     * @param parent      the parent of the child to search.
     * @param name        the name of the child to search for.
     * @return            the specified child of the parent, or <code>null
     *                    </code> if no such child exists.
     */
    public static Element getLastChild(Element parent, String name) {
        Assert.isNotNull(name);
        return getChild(parent, new String[] {name}, false);
    }

    /**
     * Returns the given element's namespace declarations. The element must
     * not be <code>null</code>.
     *
     * @param element the element whose namespace declarations are returned
     * @return        the element's namespace declarations
     */
    public static Namespaces getNamespaces(Element element) {
        Assert.isNotNull(element);
        Node parent = element.getParentNode();
        while (parent != null && !isElement(parent))
            parent = parent.getParentNode();
        Namespaces namespaces = null;
        if (parent != null)
            namespaces = getNamespaces((Element) parent);
        return getNamespaces(element, namespaces, false);
    }

    /**
     * Returns the given element's namespace declarations. The given
     * namespace declarations should be the element's parent's, or
     * <code>null</code> if the element has no parent. If
     * removeDuplicateNSDeclarations is <code>true</code>, duplicate
     * namespace declarations are removed from the element's attributes. The
     * element must not be <code>null</code>.
     *
     * @param element    the element whose namespace declaration is returned
     * @param namespaces the namespace declarations for the element's
     *                   parent's or <code>null</code> if it has no parent
     * @param removeDuplicateNSDeclarations a boolean indicating whether
     *                   duplicate namespace declarations should be removed
     *                   from the element's attributes
     * @return           the given element's namespace declarations
     */
    protected static Namespaces getNamespaces(Element element, Namespaces namespaces, boolean removeDuplicateNSDeclarations) {

        // Create a container to hold the new namespace definitions.
        Namespaces newNamespaces = null;
        if (namespaces == null)
            newNamespaces = new Namespaces();
        else
            newNamespaces = new Namespaces(namespaces);

        Vector oldAttributes = new Vector();

        // For each attribute on the given element.
        NamedNodeMap nodeMap = element.getAttributes();
        for (int i = 0; i < nodeMap.getLength(); ++i) {
            Attr attr = (Attr) nodeMap.item(i);

            // Is it a name space declaration?
            String name = attr.getName();
            if (name.startsWith(XML_PREFIX)) {
                String nsName = attr.getValue();

                // Is it setting or clearing the default namespace?
                // (i.e. has no prefix part)
                if (name.length() == XML_PREFIX.length()) {
                    if (nsName.equals("")) //$NON-NLS-1$
                        newNamespaces.setDefaultNSName(null);
                    else
                        newNamespaces.setDefaultNSName(nsName);
                } else if (name.charAt(XML_PREFIX.length()) == ':') {
                    // It is a namespace declaration.
                    String nsPrefix = name.substring(XML_PREFIX.length() + 1);
                    if (nsPrefix.length() > 0 && nsName.length() > 0) {
                        // Ensure it is in the new namespaces list.
                        newNamespaces.putNSName(nsPrefix, nsName);
                        boolean prefixExists = newNamespaces.getNSPrefix(nsName) != null;
                        if (!prefixExists)
                            newNamespaces.putNSPrefix(nsName, nsPrefix);
                        // If it is due for removal, rememebr it in the oldAttributes list.
                        if (removeDuplicateNSDeclarations && (prefixExists || nsName.equals(newNamespaces.getDefaultNSName())))
                            oldAttributes.addElement(attr);
                    }
                }
            }
        }

        // Remove all the duplicates on the given element.
        Enumeration e = oldAttributes.elements();
        while (e.hasMoreElements())
            element.removeAttributeNode((Attr) e.nextElement());

        // Answer the new list of namespaces for this element.
        return newNamespaces;
    }

    /**
     * Returns the next sibling element of the given element, or
     * <code>null</code> if no such sibling exists. Only the sibling's
     * next children are searched. The element must not be <code>null</code>.
     *
     * @param element     the element whose sibling element is being
     *                    searched for
     * @return            the sibling element, or <code>null</code>
     */
    public static Element getNextSibling(Element element) {
        Assert.isNotNull(element);
        Node sibling = element;
        do {
            sibling = sibling.getNextSibling();
        } while (sibling != null && !isElement(sibling));
        return (Element) sibling;
    }

    /**
     * Returns the first WebDAV sibling of the given element that has one of
     * the given names, or <code>null</code> if no such sibling exists.  Only
     * the sibling's next children (not the previous children) are searched.
     * The element must not be <code>null</code> and must be a WebDAV element.
     * The possible names of the sibling to search for must not be
     * <code>null</code>.
     *
     * @param element     the element whose sibling is being searched for
     * @param names       the possible names of the sibling being search for
     * @return            the sibling with the specified name, or
     *                    <code>null</code>
     */
    public static Element getNextSibling(Element element, String[] names) {
        Assert.isTrue(isDAVElement(element));
        Assert.isNotNull(names);
        Node sibling = element.getNextSibling();
        while (sibling != null) {
            for (int i = 0; i < names.length; ++i)
                if (isDAVElement(sibling, names[i]))
                    return (Element) sibling;
            sibling = sibling.getNextSibling();
        }
        return null;
    }

    /**
     * Returns the next WebDAV sibling of the given element that has the
     * given name, or <code>null</code> if no such sibling exists.  Only
     * the sibling's next children are searched. The element must not be
     * <code>null</code> and must be a WebDAV element.  The name of the
     * sibling to search for must not be <code>null</code>.
     *
     * @param element     the element whose sibling is being searched for
     * @param name       the name of the sibling being search for
     * @return            the sibling with the given name, or
     *                    <code>null</code>
     */
    public static Element getNextSibling(Element element, String name) {
        return getNextSibling(element, new String[] {name});
    }

    /**
     * Returns the local part of the given name, or <code>null</code> if its
     * name has no local part. The name must not be <code>null</code>.
     * <table>
     * <caption>Example</caption>
     * <tr>
     *   <th>name</th>
     *   <th>local name</th>
     * <tr>
     *   <td>D:foo</td>
     *   <td>foo</td>
     * <tr>
     *   <td>foo</td>
     *   <td>foo</td>
     * <tr>
     *   <td>D:</td>
     *   <td>null</td>
     * <tr>
     *   <td>:foo</td>
     *   <td>foo</td>
     * <tr>
     *   <td>:</td>
     *   <td>null</td>
     * </table>
     *
     * @param name the name whose local part is returned
     * @return     the name's local part, or <code>null</code>
     */
    public static String getNSLocalName(String name) {
        Assert.isNotNull(name);
        int p = name.lastIndexOf(':');
        if (p == -1)
            return name;
        if (p == name.length() - 1)
            return null;
        return name.substring(p + 1);
    }

    /**
     * Returns the local part of the name of the given element, or
     * <code>null</code> if its name has no local part. The element must not
     * be <code>null</code>.
     * <table>
     * <caption>Example</caption>
     * <tr>
     *   <th>tag name</th>
     *   <th>local name</th>
     * <tr>
     *   <td>D:foo</td>
     *   <td>foo</td>
     * <tr>
     *   <td>foo</td>
     *   <td>foo</td>
     * <tr>
     *   <td>D:</td>
     *   <td>null</td>
     * <tr>
     *   <td>:foo</td>
     *   <td>foo</td>
     * <tr>
     *   <td>:</td>
     *   <td>null</td>
     * </table>
     *
     * @param element the element whose local name is returned
     * @return        the element's local name, or <code>null</code>
     */
    public static String getNSLocalName(Element element) {
        Assert.isNotNull(element);
        return getNSLocalName(element.getTagName());
    }

    /**
     * Returns the URL of the given element's namespace, or
     * <code>null</code> if it has no namespace. The element must not be
     * <code>null</code>.
     *
     * @param element the element whose namespace is returned
     * @return        the namespace of the element, or <code>null</code>
     * @throws        MalformedElementException if the name of the given
     *                element has a namespace prefix which could not be
     *                resolved
     */
    public static String getNSName(Element element) throws MalformedElementException {
        Assert.isNotNull(element);
        String nsPrefix = getNSPrefix(element);
        String nsName = resolve(nsPrefix, element);
        if (nsPrefix != null && nsName == null)
            throw new MalformedElementException(Policy.bind("exception.namespacePrefixNotResolved", nsPrefix)); //$NON-NLS-1$
        return nsName;
    }

    /**
     * Returns the namespace prefix part of the given name, or
     * <code>null</code> if its name has no prefix. The name must not be
     * <code>null</code>.
     * <table>
     * <caption>Example</caption>
     * <tr>
     *   <th>name</th>
     *   <th>namespace prefix</th>
     * <tr>
     *   <td>D:foo</td>
     *   <td>D</td>
     * <tr>
     *   <td>foo</td>
     *   <td>null</td>
     * <tr>
     *   <td>D:</td>
     *   <td>D</td>
     * <tr>
     *   <td>:foo</td>
     *   <td>null</td>
     * <tr>
     *   <td>:</td>
     *   <td>null</td>
     * </table>
     *
     * @param name the name whose namespace prefix is returned
     * @return     the name's namespace prefix, or <code>null</code>
     */
    public static String getNSPrefix(String name) {
        Assert.isNotNull(name);
        int p = name.lastIndexOf(':');
        if (p <= 0)
            return null;
        return name.substring(0, p);
    }

    /**
     * Returns the namespace prefix part of the name of the given element,
     * or <code>null</code> if its name has no prefix. The element must not
     * be <code>null</code>.
     * <table>
     * <caption>Example</caption>
     * <tr>
     *   <th>tag name</th>
     *   <th>namespace prefix</th>
     * <tr>
     *   <td>D:foo</td>
     *   <td>D</td>
     * <tr>
     *   <td>foo</td>
     *   <td>null</td>
     * <tr>
     *   <td>D:</td>
     *   <td>D</td>
     * <tr>
     *   <td>:foo</td>
     *   <td>null</td>
     * <tr>
     *   <td>:</td>
     *   <td>null</td>
     * </table>
     *
     * @param element the element whose namespace prefix is returned
     * @return        the element's namespace prefix, or <code>null</code>
     */
    public static String getNSPrefix(Element element) {
        Assert.isNotNull(element);
        return getNSPrefix(element.getTagName());
    }

    /**
     * Returns a qualified name that is formed from the given element's
     * namespace name and namespace local name. The qualified name's
     * qualifier is the element's namespace name and the qualified name's
     * local name is the element's local name. The element must not be
     * <code>null</code>.
     *
     * @param     element the element whose qualified name is returned
     * @return            the qualified name that is formed from the given
     *                    element's namespace name and namespace local name
     * @exception MalformedElementException if the name of the element has a
     *            namespace prefix which could not be resolved, or the
     *            element does not have a local name
     */
    public static QualifiedName getQualifiedName(Element element) throws MalformedElementException {
        Assert.isNotNull(element);
        String nsName = getNSName(element);
        String nsLocalName = getNSLocalName(element);
        if (nsLocalName == null)
            throw new MalformedElementException(Policy.bind("exception.noLocalNameForElmt")); //$NON-NLS-1$
        return new QualifiedNameImpl(nsName, nsLocalName);
    }

    /**
     * Returns the first WebDAV sibling of the given element that has the
     * given name, or <code>null</code> if no such sibling exists. If
     * firstToLast is true, only the sibling's next children are searched,
     * otherwise, only the siblings previous children are searched. The
     * element must not be <code>null</code> and must be a WebDAV element.
     * The name of the sibling to search for must not be <code>null</code>.
     *
     * @param element the element whose sibling is being searched for
     * @param name the name of the sibling being search for
     * @param firstToLast a boolean specifying the direction to search for
     *                    the sibling among the element's siblings
     * @return            the sibling with the given name, or
     *                    <code>null</code>
     * @deprecated
     */
    public static Element getSibling(Element element, String name, boolean firstToLast) {
        Assert.isTrue(isDAVElement(element));
        Assert.isNotNull(name);
        Node sibling = element;
        do {
            if (firstToLast)
                sibling = sibling.getNextSibling();
            else
                sibling = sibling.getPreviousSibling();
        } while (sibling != null && !isDAVElement(sibling, name));
        return (Element) sibling;
    }

    /**
     * Returns the data of the given parent's first text node, or the empty
     * <code>String</code> if no such text node exists. If firstToLast is
     * true, the search for the text node starts at the parent's first
     * child, otherwise, the search starts at the parent's last child. The
     * parent must not be <code>null</code>.
     *
     * @param parent      the parent of the text node to search for
     * @param firstToLast a boolean specifying the direction to search for
     *                    the text node among the parent's children
     * @return            the data of the text node being searched for, or
     *                    the empty <code>String</code> if no such text node
     *                    exists
     * @deprecated
     */
    public static String getText(Element parent, boolean firstToLast) {
        Assert.isNotNull(parent);
        Node child = null;
        if (firstToLast)
            child = parent.getFirstChild();
        else
            child = parent.getLastChild();
        while (child != null && !isText(child)) {
            if (firstToLast)
                child = child.getNextSibling();
            else
                child = child.getPreviousSibling();
        }
        if (child != null)
            return ((Text) child).getData();
        return ""; //$NON-NLS-1$
    }

    /**
     * Returns the first WebDAV sibling of the given element that has the
     * same name as the element, or <code>null</code> if no such sibling
     * exists. If firstToLast is true, only the sibling's next children are
     * searched, otherwise, only the siblings previous children are
     * searched. The element must not be <code>null</code> and must be a
     * WebDAV element.
     *
     * @param element     the element whose sibling is being searched for
     * @param firstToLast a boolean specifying the direction to search for
     *                    the sibling among the element's siblings
     * @return            the sibling with the same name as the given
     *                    element, or <code>null</code> if no such element
     *                    exists
     */
    public static Element getTwin(Element element, boolean firstToLast) {
        Assert.isTrue(isDAVElement(element));
        String name = getNSLocalName(element);
        return getSibling(element, name, firstToLast);
    }

    public static boolean hasChild(Element parent, QualifiedName childName) throws MalformedElementException {
        // Get the first candidate.
        Node child = parent.getFirstChild();
        // While there are children left to consider.
        while (child != null) {
            if (child instanceof Element) {
                QualifiedName name = getQualifiedName((Element) child);
                if (name.equals(childName))
                    return true;
            }
            // Try the next child.
            child = child.getNextSibling();
        }
        // A matching child was not found.
        return false;
    }

    /**
     * Creates a WebDAV element with the given name and inserts it before
     * the given sibling. Returns the new sibling. The sibling must not be
     * <code>null</code> and must be a WebDAV element. The name of the
     * new sibling must not be <code>null</code>.
     *
     * @param sibling the existing sibling element
     * @param name    the name of the new sibling element that is created
     *                and inserted before the existing sibling element
     * @return        the new sibling element that is created
     */
    public static Element insertBefore(Element sibling, String name) {
        Assert.isTrue(isDAVElement(sibling));
        Assert.isNotNull(name);
        String nsPrefix = getNSPrefix(sibling);
        String tagName = nsPrefix == null ? name : nsPrefix + ":" + name; //$NON-NLS-1$
        Element newSibling = sibling.getOwnerDocument().createElement(tagName);
        sibling.getParentNode().insertBefore(newSibling, sibling);
        return newSibling;
    }

    /**
     * Creates a WebDAV element with the given name and inserts it before
     * the given sibling. In addition, a text node created from the given
     * data and appended to the new sibling. Returns the new sibling. The
     * sibling must not be <code>null</code> and must be a WebDAV element.
     * The name of the new sibling must not be <code>null</code>. The data
     * must not be <code>null</code>.
     *
     * @param sibling the existing sibling element
     * @param name    the name of the sibling element that is created and
     *                inserted before the existing sibling element
     * @param data    the data of the text node which is created and
     *                appended to the new sibling
     * @return        the new sibling element that is created
     */
    public static Element insertBefore(Element sibling, String name, String data) {
        Assert.isTrue(isDAVElement(sibling));
        Assert.isNotNull(name);
        Assert.isNotNull(data);
        Element child = insertBefore(sibling, name);
        child.appendChild(child.getOwnerDocument().createTextNode(data));
        return child;
    }

    /**
     * Returns a boolean indicating whether or not the given node is a
     * WebDAV element. The node may be <code>null</code> in which case
     * <code>false</code> is returned.
     *
     * @param node a node, or <code>null</code>
     * @return     a boolean indicating whether or not the given node is a
     *             WebDAV element
     */
    public static boolean isDAVElement(Node node) {
        if (node == null || node.getNodeType() != Node.ELEMENT_NODE)
            return false;
        try {
            if (!DAV_NS.equals(getNSName((Element) node)))
                return false;
        } catch (MalformedElementException e) {
            return false;
        }
        return true;
    }

    /**
     * Returns a boolean indicating whether or not the given node is a
     * WebDAV element with the given name. The node may be <code>null</code>
     * in which case <code>false</code> is returned. The name must not be
     * <code>null</code>.
     *
     * @param node a node, or <code>null</code>
     * @return     a boolean indicating whether or not the given node is a
     *             WebDAV element with the given name
     */
    public static boolean isDAVElement(Node node, String name) {
        Assert.isNotNull(name);
        if (node == null || node.getNodeType() != Node.ELEMENT_NODE)
            return false;
        try {
            Element element = (Element) node;
            if (!name.equals(getNSLocalName(element)) || !DAV_NS.equals(getNSName(element))) {
                return false;
            }
        } catch (MalformedElementException e) {
            return false;
        }
        return true;
    }

    /**
     * Returns a boolean indicating whether or not the given node is an
     * element. The node may be <code>null</code> in which case
     * <code>false</code> is returned.
     *
     * @param node a node, or <code>null</code>
     * @return     a boolean indicating whether or not the given node is an
     *             element
     */
    public static boolean isElement(Node node) {
        if (node == null || node.getNodeType() != Node.ELEMENT_NODE)
            return false;
        return true;
    }

    /**
     * Returns a boolean indicating whether or not the given node is a text
     * node. The node may be <code>null</code> in which case
     * <code>false</code> is returned.
     *
     * @param node a node, or <code>null</code>
     * @return     a boolean indicating whether or not the given node is a
     *             text node
     */
    public static boolean isText(Node node) {
        return (node != null) && (node.getNodeType() == Node.TEXT_NODE);
    }

    public static void makeNSStandalone(Element element) {
        Assert.isTrue(false, Policy.bind("assert.notImplemented")); //$NON-NLS-1$
    }

    /**
     * Removes redundant namespace declarations from this element and all
     * its children to maximum depth. The element must not be
     * <code>null</code>.
     *
     * @param element the element whose namespace declarations are to be
     *                reduced
     * @return        the element, or a new element with an equivalent
     *                qualified name but whose tag name has changed
     * @throws        MalformedElementException if the name of the given
     *                element, or one of its attributes, has a namespace
     *                prefix which could not be resolved
     */
    public static Element reduceNS(Element element) throws MalformedElementException {
        return (Element) reduceNS(element, null);
    }

    /**
     * Removes redundant namespace declarations from the given node and all
     * its children to maximum depth. The node must not be
     * <code>null</code>.
     *
     * @param node the node whose namespace declarations are to be reduced
     * @return     the node, or a new node (element) with an equivalent
     *             qualified name but whose tag name has changed
     * @throws     MalformedElementException if the name of the given node
     *             (element), or one of its attributes, has a namespace
     *             prefix which could not be resolved
     */
    public static Node reduceNS(Node node, Namespaces parentNamespaces) throws MalformedElementException {
        Namespaces namespaces = parentNamespaces;

        if (node.getNodeType() == Node.ELEMENT_NODE) {
            Element element = (Element) node;
            namespaces = getNamespaces(element, parentNamespaces, false);

            String nsPrefix = getNSPrefix(element);
            String nsLocalName = getNSLocalName(element);

            if (nsPrefix != null) {
                String nsName = namespaces.getNSName(nsPrefix);
                ensureNotNull(Policy.bind("ensure.missingNamespaceForPrefix", nsPrefix), nsName); //$NON-NLS-1$

                String tagName = null;
                if (nsName.equals(namespaces.getDefaultNSName())) {
                    tagName = nsLocalName;
                } else {
                    tagName = namespaces.getNSPrefix(nsName) + ":" + nsLocalName; //$NON-NLS-1$
                }

                if (!tagName.equals(element.getTagName())) {
                    Document document = element.getOwnerDocument();
                    Element newElement = document.createElement(tagName);
                    NamedNodeMap nodeMap = element.getAttributes();
                    for (int i = 0; i < nodeMap.getLength(); ++i) {
                        Attr attr = (Attr) nodeMap.item(i);
                        newElement.setAttribute(attr.getName(), attr.getValue());
                    }
                    Node child = element.getFirstChild();
                    while (child != null) {
                        element.removeChild(child);
                        newElement.appendChild(child);
                        child = element.getFirstChild();
                    }
                    element.getParentNode().replaceChild(newElement, element);
                    element = newElement;
                }
            }

            Vector oldAttributes = new Vector();
            Vector newAttributes = new Vector();
            NamedNodeMap nodeMap = element.getAttributes();
            for (int i = 0; i < nodeMap.getLength(); ++i) {
                Attr attr = (Attr) nodeMap.item(i);
                String name = attr.getName();
                String value = attr.getValue();
                String newName = name;
                nsPrefix = getNSPrefix(name);
                nsLocalName = getNSLocalName(name);

                if (nsPrefix != null && !nsPrefix.equals(XML_PREFIX)) {
                    String nsName = namespaces.getNSName(nsPrefix);
                    ensureNotNull(Policy.bind("ensure.missingNamespaceForPrefix", nsPrefix), nsName); //$NON-NLS-1$
                    String newNSPrefix = namespaces.getNSPrefix(nsName);
                    if (!newNSPrefix.equals(nsPrefix)) {
                        newName = newNSPrefix + ":" + nsLocalName; //$NON-NLS-1$
                    }
                }

                boolean newAttribute = true;
                if (parentNamespaces != null) {
                    if (nsPrefix == null && XML_PREFIX.equals(nsLocalName)) {
                        if (value.equals(parentNamespaces.getDefaultNSName())) {
                            newAttribute = false;
                        }
                    }
                    if (nsPrefix != null && XML_PREFIX.equals(nsPrefix)) {
                        if (parentNamespaces.getNSPrefix(value) != null) {
                            newAttribute = false;
                        }
                    }
                }

                oldAttributes.addElement(attr);
                if (newAttribute) {
                    newAttributes.addElement(new String[] {newName, value});
                }
            }

            Enumeration oldAttrs = oldAttributes.elements();
            while (oldAttrs.hasMoreElements()) {
                element.removeAttributeNode((Attr) oldAttrs.nextElement());
            }

            Enumeration newAttrs = newAttributes.elements();
            while (newAttrs.hasMoreElements()) {
                String[] newAttr = (String[]) newAttrs.nextElement();
                element.setAttribute(newAttr[0], newAttr[1]);
            }

            node = element;
        }

        Node child = node.getFirstChild();
        while (child != null) {
            child = reduceNS(child, namespaces);
            child = child.getNextSibling();
        }

        return node;
    }

    /**
     * Resolves the given namespace prefix in the namespace of the given
     * element. If the given prefix is <code>null</code>, the default
     * namespace is resolved. Returns the URL of the namespace, or
     * <code>null</code> if the prefix could not be resolved.
     *
     * @param prefix  the namespace prefix to be resolved, or
     *                <code>null<code> for the default namespace
     * @param element the element supplying the namespace
     * @return        the URL of the namespace, or <code>null</code> if the
     *                prefix could not be resolved
     */
    public static String resolve(String prefix, Element element) {
        Assert.isNotNull(element);

        /* The prefix xml is by definition bound to the namespace name
         * <code>XML_NS_NAME</code>.
         */
        if (XML_NS_PREFIX.equals(prefix)) {
            return XML_NS_NAME;
        }

        /* Search from given element up parent chain to root (document)
         * looking for a XML namespace declaration (represented as
         * an element attribute with a name beginning in
         * XML_PREFIX ("xmlns")).
         */
        Node current = element;
        do {
            NamedNodeMap attrs = current.getAttributes();
            int n = attrs.getLength();
            for (int i = 0; i < n; i++) {
                Attr attr = (Attr) attrs.item(i);
                String name = attr.getName();
                if (name.startsWith(XML_PREFIX)) {
                    if (name.length() == XML_PREFIX.length()) {
                        // no prefix e.g., xmlns="foo:"
                        if (prefix == null) {
                            String nsName = attr.getValue();
                            if (nsName.equals("")) { //$NON-NLS-1$
                                return null;
                            }
                            return nsName;
                        }
                    } else {
                        if (prefix != null && name.equals(XML_PREFIX + ":" + prefix)) { //$NON-NLS-1$
                            return attr.getValue();
                        }
                    }
                }
            }
            do {
                current = current.getParentNode();
            } while (current != null && current.getNodeType() != Node.ELEMENT_NODE);
        } while (current != null);
        return null;
    }

    /**
     * <p>Creates a WebDAV element with the given name and sets it as a
     * child of the given parent. Returns the child element.
     * <p>Children are positioned in the order specified by the given names.
     * If a child with the same name as the child already exist, the child
     * is replaced. If firstToLast is true, the search for the child's
     * position starts at the parent's first child, otherwise, the search
     * starts at the parent's last child.
     * <p>The parent must not be <code>null</code> and must be a WebDAV
     * element.  The child's name must not be <code>null</code>.  The
     * parent's valid child names must not be <code>null</code>, and must
     * contain the name of the child.
     *
     * @param parent      the parent to which the child is added
     * @param name        the name of the child which is created and added
     *                    to the parent
     * @param names       the ordered collection of valid child names for
     *                    the parent
     * @param firstToLast a boolean specifying the direction to search for
     *                    the child's position among the parent's children
     * @return            the child element that is created
     */
    public static Element setChild(Element parent, String name, String[] names, boolean firstToLast) {
        Assert.isTrue(isDAVElement(parent));
        Assert.isNotNull(name);
        Assert.isNotNull(names);
        String nsPrefix = getNSPrefix(parent);
        String tagName = nsPrefix == null ? name : nsPrefix + ":" + name; //$NON-NLS-1$
        Element child = parent.getOwnerDocument().createElement(tagName);
        setChild(parent, child, names, firstToLast);
        return child;
    }

    /**
     * <p>Creates a WebDAV element with the given name and sets it as a
     * child of the given parent. In addition, a text node created from the
     * given data is created and appended to the child.  Returns the child
     * element.
     * <p>Children are positioned in the order specified by the given names.
     * If a child with the same name already exists, it is replaced. If
     * firstToLast is true, the search for the child's position starts at the
     * parent's first child, otherwise, the search starts at the parent's
     * last child.
     * <p>The parent must not be <code>null</code> and must be a WebDAV
     * element.  The child's name and data must not be <code>null</code>.
     * The parent's valid child names must not be <code>null</code>, and
     * must contain the name of the child.
     *
     * @param parent      the parent to which the child is added
     * @param name        the name of the child which is created and added
     *                    to the parent
     * @param data        the data of the text node which is created and
     *                    added to the child
     * @param names       the ordered collection of valid child names for
     *                    the parent
     * @param firstToLast a boolean specifying the direction to search for
     *                    the child's position among the parent's children
     * @return            the child element that is created
     */
    public static Element setChild(Element parent, String name, String data, String[] names, boolean firstToLast) {
        Assert.isTrue(isDAVElement(parent));
        Assert.isNotNull(name);
        Assert.isNotNull(data);
        Assert.isNotNull(names);
        Element child = setChild(parent, name, names, firstToLast);
        child.appendChild(parent.getOwnerDocument().createTextNode(data));
        return child;
    }

    /**
     * <p>Sets the given child element as a child of the given parent.
     * <p>Children are positioned in the order specified by the given names.
     * If a child with the same name already exists, it is replaced. If
     * firstToLast is true, the search for the child's position starts at
     * the parent's first child, otherwise, the search starts at the
     * parent's last child.
     * <p>The parent must not be <code>null</code> and must be a WebDAV
     * element. The child must not be null and its namespace prefix must
     * resolve to the WebDAV namespace URL in the parent. The parent's valid
     * child names must not be <code>null</code>, and must contain the name
     * of the child.
     *
     * @param parent      the parent to which the child is added
     * @param child       the child which is added to the parent
     * @param names       the ordered collection of valid child names for
     *                    the parent
     * @param firstToLast a boolean specifying the direction to search for
     *                    the child's position among the parent's children
     */
    public static void setChild(Element parent, Element child, String[] names, boolean firstToLast) {
        Assert.isTrue(isDAVElement(parent));
        Assert.isNotNull(child);
        Assert.isTrue(DAV_NS.equals(resolve(getNSPrefix(child), parent)));
        Assert.isNotNull(names);

        boolean found = false;
        String name = getNSLocalName(child);
        for (int i = 0; !found && i < names.length; ++i) {
            found = names[i].equals(name);
        }
        Assert.isTrue(found);

        Node sibling = getChild(parent, name, names, firstToLast);

        if (isDAVElement(sibling, name)) {
            parent.replaceChild(child, sibling);
        } else if (firstToLast) {
            if (sibling == null) {
                parent.appendChild(child);
            } else {
                parent.insertBefore(child, sibling);
            }
        } else {
            Node refChild = null;
            if (sibling == null) {
                refChild = parent.getFirstChild();
            } else {
                refChild = sibling.getNextSibling();
            }
            if (refChild == null) {
                parent.appendChild(child);
            } else {
                parent.insertBefore(child, refChild);
            }
        }
    }
}
