Remove dependency on controller config-util
[netconf.git] / netconf / netconf-api / src / main / java / org / opendaylight / netconf / api / xml / XmlElement.java
diff --git a/netconf/netconf-api/src/main/java/org/opendaylight/netconf/api/xml/XmlElement.java b/netconf/netconf-api/src/main/java/org/opendaylight/netconf/api/xml/XmlElement.java
new file mode 100644 (file)
index 0000000..ad90fb3
--- /dev/null
@@ -0,0 +1,503 @@
+/*
+ * Copyright (c) 2015 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.api.xml;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.opendaylight.netconf.api.DocumentedException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.w3c.dom.Text;
+import org.xml.sax.SAXException;
+
+public final class XmlElement {
+
+    public static final String DEFAULT_NAMESPACE_PREFIX = "";
+
+    private final Element element;
+    private static final Logger LOG = LoggerFactory.getLogger(XmlElement.class);
+
+    private XmlElement(final Element element) {
+        this.element = element;
+    }
+
+    public static XmlElement fromDomElement(final Element element) {
+        return new XmlElement(element);
+    }
+
+    public static XmlElement fromDomDocument(final Document xml) {
+        return new XmlElement(xml.getDocumentElement());
+    }
+
+    public static XmlElement fromString(final String str) throws DocumentedException {
+        try {
+            return new XmlElement(XmlUtil.readXmlToElement(str));
+        } catch (IOException | SAXException e) {
+            throw DocumentedException.wrap(e);
+        }
+    }
+
+    public static XmlElement fromDomElementWithExpected(final Element element, final String expectedName)
+            throws DocumentedException {
+        XmlElement xmlElement = XmlElement.fromDomElement(element);
+        xmlElement.checkName(expectedName);
+        return xmlElement;
+    }
+
+    public static XmlElement fromDomElementWithExpected(final Element element, final String expectedName,
+            final String expectedNamespace) throws DocumentedException {
+        XmlElement xmlElement = XmlElement.fromDomElementWithExpected(element, expectedName);
+        xmlElement.checkNamespace(expectedNamespace);
+        return xmlElement;
+    }
+
+    private Map<String, String> extractNamespaces() throws DocumentedException {
+        Map<String, String> namespaces = new HashMap<>();
+        NamedNodeMap attributes = element.getAttributes();
+        for (int i = 0; i < attributes.getLength(); i++) {
+            Node attribute = attributes.item(i);
+            String attribKey = attribute.getNodeName();
+            if (attribKey.startsWith(XmlUtil.XMLNS_ATTRIBUTE_KEY)) {
+                String prefix;
+                if (attribKey.equals(XmlUtil.XMLNS_ATTRIBUTE_KEY)) {
+                    prefix = DEFAULT_NAMESPACE_PREFIX;
+                } else {
+                    if (!attribKey.startsWith(XmlUtil.XMLNS_ATTRIBUTE_KEY + ":")) {
+                        throw new DocumentedException("Attribute doesn't start with :",
+                                DocumentedException.ErrorType.APPLICATION,
+                                DocumentedException.ErrorTag.INVALID_VALUE,
+                                DocumentedException.ErrorSeverity.ERROR);
+                    }
+                    prefix = attribKey.substring(XmlUtil.XMLNS_ATTRIBUTE_KEY.length() + 1);
+                }
+                namespaces.put(prefix, attribute.getNodeValue());
+            }
+        }
+
+        // namespace does not have to be defined on this element but inherited
+        if (!namespaces.containsKey(DEFAULT_NAMESPACE_PREFIX)) {
+            Optional<String> namespaceOptionally = getNamespaceOptionally();
+            if (namespaceOptionally.isPresent()) {
+                namespaces.put(DEFAULT_NAMESPACE_PREFIX, namespaceOptionally.get());
+            }
+        }
+
+        return namespaces;
+    }
+
+    public void checkName(final String expectedName) throws UnexpectedElementException {
+        if (!getName().equals(expectedName)) {
+            throw new UnexpectedElementException(String.format("Expected %s xml element but was %s", expectedName,
+                    getName()),
+                    DocumentedException.ErrorType.APPLICATION,
+                    DocumentedException.ErrorTag.OPERATION_FAILED,
+                    DocumentedException.ErrorSeverity.ERROR);
+        }
+    }
+
+    public void checkNamespaceAttribute(final String expectedNamespace)
+            throws UnexpectedNamespaceException, MissingNameSpaceException {
+        if (!getNamespaceAttribute().equals(expectedNamespace)) {
+            throw new UnexpectedNamespaceException(String.format("Unexpected namespace %s should be %s",
+                    getNamespaceAttribute(),
+                    expectedNamespace),
+                    DocumentedException.ErrorType.APPLICATION,
+                    DocumentedException.ErrorTag.OPERATION_FAILED,
+                    DocumentedException.ErrorSeverity.ERROR);
+        }
+    }
+
+    public void checkNamespace(final String expectedNamespace)
+            throws UnexpectedNamespaceException, MissingNameSpaceException {
+        if (!getNamespace().equals(expectedNamespace)) {
+            throw new UnexpectedNamespaceException(String.format("Unexpected namespace %s should be %s",
+                    getNamespace(),
+                    expectedNamespace),
+                    DocumentedException.ErrorType.APPLICATION,
+                    DocumentedException.ErrorTag.OPERATION_FAILED,
+                    DocumentedException.ErrorSeverity.ERROR);
+        }
+    }
+
+    public String getName() {
+        final String localName = element.getLocalName();
+        if (!Strings.isNullOrEmpty(localName)) {
+            return localName;
+        }
+        return element.getTagName();
+    }
+
+    public String getAttribute(final String attributeName) {
+        return element.getAttribute(attributeName);
+    }
+
+    public String getAttribute(final String attributeName, final String namespace) {
+        return element.getAttributeNS(namespace, attributeName);
+    }
+
+    public NodeList getElementsByTagName(final String name) {
+        return element.getElementsByTagName(name);
+    }
+
+    public void appendChild(final Element toAppend) {
+        this.element.appendChild(toAppend);
+    }
+
+    public Element getDomElement() {
+        return element;
+    }
+
+    public Map<String, Attr> getAttributes() {
+
+        Map<String, Attr> mappedAttributes = Maps.newHashMap();
+
+        NamedNodeMap attributes = element.getAttributes();
+        for (int i = 0; i < attributes.getLength(); i++) {
+            Attr attr = (Attr) attributes.item(i);
+            mappedAttributes.put(attr.getNodeName(), attr);
+        }
+
+        return mappedAttributes;
+    }
+
+    /**
+     * Non recursive.
+     */
+    private List<XmlElement> getChildElementsInternal(final ElementFilteringStrategy strat) {
+        NodeList childNodes = element.getChildNodes();
+        final List<XmlElement> result = new ArrayList<>();
+        for (int i = 0; i < childNodes.getLength(); i++) {
+            Node item = childNodes.item(i);
+            if (!(item instanceof Element)) {
+                continue;
+            }
+            if (strat.accept((Element) item)) {
+                result.add(new XmlElement((Element) item));
+            }
+        }
+
+        return result;
+    }
+
+    public List<XmlElement> getChildElements() {
+        return getChildElementsInternal(e -> true);
+    }
+
+    /**
+     * Returns the child elements for the given tag.
+     *
+     * @param tagName tag name without prefix
+     * @return List of child elements
+     */
+    public List<XmlElement> getChildElements(final String tagName) {
+        return getChildElementsInternal(e -> {
+            // localName returns pure localName without prefix
+            return e.getLocalName().equals(tagName);
+        });
+    }
+
+    public List<XmlElement> getChildElementsWithinNamespace(final String childName, final String namespace) {
+        return Lists.newArrayList(Collections2.filter(getChildElementsWithinNamespace(namespace),
+            xmlElement -> xmlElement.getName().equals(childName)));
+    }
+
+    public List<XmlElement> getChildElementsWithinNamespace(final String namespace) {
+        return getChildElementsInternal(e -> {
+            try {
+                return XmlElement.fromDomElement(e).getNamespace().equals(namespace);
+            } catch (final MissingNameSpaceException e1) {
+                return false;
+            }
+        });
+    }
+
+    public Optional<XmlElement> getOnlyChildElementOptionally(final String childName) {
+        List<XmlElement> nameElements = getChildElements(childName);
+        if (nameElements.size() != 1) {
+            return Optional.absent();
+        }
+        return Optional.of(nameElements.get(0));
+    }
+
+    public Optional<XmlElement> getOnlyChildElementOptionally(final String childName, final String namespace) {
+        List<XmlElement> children = getChildElementsWithinNamespace(namespace);
+        children = Lists.newArrayList(Collections2.filter(children,
+            xmlElement -> xmlElement.getName().equals(childName)));
+        if (children.size() != 1) {
+            return Optional.absent();
+        }
+        return Optional.of(children.get(0));
+    }
+
+    public Optional<XmlElement> getOnlyChildElementOptionally() {
+        List<XmlElement> children = getChildElements();
+        if (children.size() != 1) {
+            return Optional.absent();
+        }
+        return Optional.of(children.get(0));
+    }
+
+    public XmlElement getOnlyChildElementWithSameNamespace(final String childName) throws  DocumentedException {
+        return getOnlyChildElement(childName, getNamespace());
+    }
+
+    public XmlElement getOnlyChildElementWithSameNamespace() throws DocumentedException {
+        XmlElement childElement = getOnlyChildElement();
+        childElement.checkNamespace(getNamespace());
+        return childElement;
+    }
+
+    public Optional<XmlElement> getOnlyChildElementWithSameNamespaceOptionally(final String childName) {
+        Optional<String> namespace = getNamespaceOptionally();
+        if (namespace.isPresent()) {
+            List<XmlElement> children = getChildElementsWithinNamespace(namespace.get());
+            children = Lists.newArrayList(Collections2.filter(children,
+                xmlElement -> xmlElement.getName().equals(childName)));
+            if (children.size() != 1) {
+                return Optional.absent();
+            }
+            return Optional.of(children.get(0));
+        }
+        return Optional.absent();
+    }
+
+    public Optional<XmlElement> getOnlyChildElementWithSameNamespaceOptionally() {
+        Optional<XmlElement> child = getOnlyChildElementOptionally();
+        if (child.isPresent()
+                && child.get().getNamespaceOptionally().isPresent()
+                && getNamespaceOptionally().isPresent()
+                && getNamespaceOptionally().get().equals(child.get().getNamespaceOptionally().get())) {
+            return child;
+        }
+        return Optional.absent();
+    }
+
+    public XmlElement getOnlyChildElement(final String childName, final String namespace) throws DocumentedException {
+        List<XmlElement> children = getChildElementsWithinNamespace(namespace);
+        children = Lists.newArrayList(Collections2.filter(children,
+            xmlElement -> xmlElement.getName().equals(childName)));
+        if (children.size() != 1) {
+            throw new DocumentedException(String.format("One element %s:%s expected in %s but was %s", namespace,
+                    childName, toString(), children.size()),
+                    DocumentedException.ErrorType.APPLICATION,
+                    DocumentedException.ErrorTag.INVALID_VALUE,
+                    DocumentedException.ErrorSeverity.ERROR);
+        }
+
+        return children.get(0);
+    }
+
+    public XmlElement getOnlyChildElement(final String childName) throws DocumentedException {
+        List<XmlElement> nameElements = getChildElements(childName);
+        if (nameElements.size() != 1) {
+            throw new DocumentedException("One element " + childName + " expected in " + toString(),
+                    DocumentedException.ErrorType.APPLICATION,
+                    DocumentedException.ErrorTag.INVALID_VALUE,
+                    DocumentedException.ErrorSeverity.ERROR);
+        }
+        return nameElements.get(0);
+    }
+
+    public XmlElement getOnlyChildElement() throws DocumentedException {
+        List<XmlElement> children = getChildElements();
+        if (children.size() != 1) {
+            throw new DocumentedException(String.format("One element expected in %s but was %s", toString(),
+                    children.size()),
+                    DocumentedException.ErrorType.APPLICATION,
+                    DocumentedException.ErrorTag.INVALID_VALUE,
+                    DocumentedException.ErrorSeverity.ERROR);
+        }
+        return children.get(0);
+    }
+
+    public String getTextContent() throws DocumentedException {
+        NodeList childNodes = element.getChildNodes();
+        if (childNodes.getLength() == 0) {
+            return DEFAULT_NAMESPACE_PREFIX;
+        }
+        for (int i = 0; i < childNodes.getLength(); i++) {
+            Node textChild = childNodes.item(i);
+            if (textChild instanceof Text) {
+                String content = textChild.getTextContent();
+                return content.trim();
+            }
+        }
+        throw new DocumentedException(getName() + " should contain text.",
+                DocumentedException.ErrorType.APPLICATION,
+                DocumentedException.ErrorTag.INVALID_VALUE,
+                DocumentedException.ErrorSeverity.ERROR
+        );
+    }
+
+    public Optional<String> getOnlyTextContentOptionally() {
+        // only return text content if this node has exactly one Text child node
+        if (element.getChildNodes().getLength() == 1) {
+            Node item = element.getChildNodes().item(0);
+            if (item instanceof Text) {
+                return Optional.of(((Text) item).getWholeText());
+            }
+        }
+        return Optional.absent();
+    }
+
+    public String getNamespaceAttribute() throws MissingNameSpaceException {
+        String attribute = element.getAttribute(XmlUtil.XMLNS_ATTRIBUTE_KEY);
+        if (attribute.isEmpty() || attribute.equals(DEFAULT_NAMESPACE_PREFIX)) {
+            throw new MissingNameSpaceException(String.format("Element %s must specify namespace",
+                    toString()),
+                    DocumentedException.ErrorType.APPLICATION,
+                    DocumentedException.ErrorTag.OPERATION_FAILED,
+                    DocumentedException.ErrorSeverity.ERROR);
+        }
+        return attribute;
+    }
+
+    public Optional<String> getNamespaceAttributeOptionally() {
+        String attribute = element.getAttribute(XmlUtil.XMLNS_ATTRIBUTE_KEY);
+        if (attribute.isEmpty() || attribute.equals(DEFAULT_NAMESPACE_PREFIX)) {
+            return Optional.absent();
+        }
+        return Optional.of(attribute);
+    }
+
+    public Optional<String> getNamespaceOptionally() {
+        String namespaceURI = element.getNamespaceURI();
+        if (Strings.isNullOrEmpty(namespaceURI)) {
+            return Optional.absent();
+        } else {
+            return Optional.of(namespaceURI);
+        }
+    }
+
+    public String getNamespace() throws MissingNameSpaceException {
+        Optional<String> namespaceURI = getNamespaceOptionally();
+        if (!namespaceURI.isPresent()) {
+            throw new MissingNameSpaceException(String.format("No namespace defined for %s", this),
+                    DocumentedException.ErrorType.APPLICATION,
+                    DocumentedException.ErrorTag.OPERATION_FAILED,
+                    DocumentedException.ErrorSeverity.ERROR);
+        }
+        return namespaceURI.get();
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("XmlElement{");
+        sb.append("name='").append(getName()).append('\'');
+        if (element.getNamespaceURI() != null) {
+            try {
+                sb.append(", namespace='").append(getNamespace()).append('\'');
+            } catch (final MissingNameSpaceException e) {
+                LOG.trace("Missing namespace for element.");
+            }
+        }
+        sb.append('}');
+        return sb.toString();
+    }
+
+    /**
+     * Search for element's attributes defining namespaces. Look for the one
+     * namespace that matches prefix of element's text content. E.g.
+     *
+     * <pre>
+     * &lt;type
+     * xmlns:th-java="urn:opendaylight:params:xml:ns:yang:controller:threadpool:impl"&gt;
+     *     th-java:threadfactory-naming&lt;/type&gt;
+     * </pre>
+     *
+     * <p>
+     * returns {"th-java","urn:.."}. If no prefix is matched, then default
+     * namespace is returned with empty string as key. If no default namespace
+     * is found value will be null.
+     */
+    public Map.Entry<String/* prefix */, String/* namespace */> findNamespaceOfTextContent()
+            throws DocumentedException {
+        Map<String, String> namespaces = extractNamespaces();
+        String textContent = getTextContent();
+        int indexOfColon = textContent.indexOf(':');
+        String prefix;
+        if (indexOfColon > -1) {
+            prefix = textContent.substring(0, indexOfColon);
+        } else {
+            prefix = DEFAULT_NAMESPACE_PREFIX;
+        }
+        if (!namespaces.containsKey(prefix)) {
+            throw new IllegalArgumentException("Cannot find namespace for " + XmlUtil.toString(element)
+                + ". Prefix from content is " + prefix + ". Found namespaces " + namespaces);
+        }
+        return Maps.immutableEntry(prefix, namespaces.get(prefix));
+    }
+
+    public List<XmlElement> getChildElementsWithSameNamespace(final String childName) throws MissingNameSpaceException {
+        List<XmlElement> children = getChildElementsWithinNamespace(getNamespace());
+        return Lists.newArrayList(Collections2.filter(children, xmlElement -> xmlElement.getName().equals(childName)));
+    }
+
+    public void checkUnrecognisedElements(final List<XmlElement> recognisedElements,
+                                          final XmlElement... additionalRecognisedElements) throws DocumentedException {
+        List<XmlElement> childElements = getChildElements();
+        childElements.removeAll(recognisedElements);
+        for (XmlElement additionalRecognisedElement : additionalRecognisedElements) {
+            childElements.remove(additionalRecognisedElement);
+        }
+        if (!childElements.isEmpty()) {
+            throw new DocumentedException(String.format("Unrecognised elements %s in %s", childElements, this),
+                    DocumentedException.ErrorType.APPLICATION,
+                    DocumentedException.ErrorTag.INVALID_VALUE,
+                    DocumentedException.ErrorSeverity.ERROR);
+        }
+    }
+
+    public void checkUnrecognisedElements(final XmlElement... additionalRecognisedElements) throws DocumentedException {
+        checkUnrecognisedElements(Collections.<XmlElement>emptyList(), additionalRecognisedElements);
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+
+        XmlElement that = (XmlElement) obj;
+
+        return element.isEqualNode(that.element);
+
+    }
+
+    @Override
+    public int hashCode() {
+        return element.hashCode();
+    }
+
+    public boolean hasNamespace() {
+        return getNamespaceAttributeOptionally().isPresent() || getNamespaceOptionally().isPresent();
+    }
+
+    private interface ElementFilteringStrategy {
+        boolean accept(Element element);
+    }
+}