c4346ee7e1d59f990e2a95fca3d23dee39cdeee3
[netconf.git] / netconf / netconf-api / src / main / java / org / opendaylight / netconf / api / xml / XmlElement.java
1 /*
2  * Copyright (c) 2015 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.netconf.api.xml;
9
10 import com.google.common.base.Strings;
11 import com.google.common.collect.Collections2;
12 import com.google.common.collect.Lists;
13 import java.io.IOException;
14 import java.util.AbstractMap.SimpleImmutableEntry;
15 import java.util.ArrayList;
16 import java.util.Collections;
17 import java.util.HashMap;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Optional;
21 import javax.xml.XMLConstants;
22 import org.opendaylight.netconf.api.DocumentedException;
23 import org.opendaylight.yangtools.yang.common.ErrorSeverity;
24 import org.opendaylight.yangtools.yang.common.ErrorTag;
25 import org.opendaylight.yangtools.yang.common.ErrorType;
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
28 import org.w3c.dom.Attr;
29 import org.w3c.dom.Document;
30 import org.w3c.dom.Element;
31 import org.w3c.dom.NamedNodeMap;
32 import org.w3c.dom.Node;
33 import org.w3c.dom.NodeList;
34 import org.w3c.dom.Text;
35 import org.xml.sax.SAXException;
36
37 public final class XmlElement {
38
39     public static final String DEFAULT_NAMESPACE_PREFIX = "";
40
41     private final Element element;
42     private static final Logger LOG = LoggerFactory.getLogger(XmlElement.class);
43
44     private XmlElement(final Element element) {
45         this.element = element;
46     }
47
48     public static XmlElement fromDomElement(final Element element) {
49         return new XmlElement(element);
50     }
51
52     public static XmlElement fromDomDocument(final Document xml) {
53         return new XmlElement(xml.getDocumentElement());
54     }
55
56     public static XmlElement fromString(final String str) throws DocumentedException {
57         try {
58             return new XmlElement(XmlUtil.readXmlToElement(str));
59         } catch (IOException | SAXException e) {
60             throw DocumentedException.wrap(e);
61         }
62     }
63
64     public static XmlElement fromDomElementWithExpected(final Element element, final String expectedName)
65             throws DocumentedException {
66         XmlElement xmlElement = XmlElement.fromDomElement(element);
67         xmlElement.checkName(expectedName);
68         return xmlElement;
69     }
70
71     public static XmlElement fromDomElementWithExpected(final Element element, final String expectedName,
72             final String expectedNamespace) throws DocumentedException {
73         XmlElement xmlElement = XmlElement.fromDomElementWithExpected(element, expectedName);
74         xmlElement.checkNamespace(expectedNamespace);
75         return xmlElement;
76     }
77
78     private Map<String, String> extractNamespaces() throws DocumentedException {
79         Map<String, String> namespaces = new HashMap<>();
80         NamedNodeMap attributes = element.getAttributes();
81         for (int i = 0; i < attributes.getLength(); i++) {
82             Node attribute = attributes.item(i);
83             String attribKey = attribute.getNodeName();
84             if (attribKey.startsWith(XMLConstants.XMLNS_ATTRIBUTE)) {
85                 String prefix;
86                 if (attribKey.equals(XMLConstants.XMLNS_ATTRIBUTE)) {
87                     prefix = DEFAULT_NAMESPACE_PREFIX;
88                 } else {
89                     if (!attribKey.startsWith(XMLConstants.XMLNS_ATTRIBUTE + ":")) {
90                         throw new DocumentedException("Attribute doesn't start with :",
91                                 ErrorType.APPLICATION, ErrorTag.INVALID_VALUE, ErrorSeverity.ERROR);
92                     }
93                     prefix = attribKey.substring(XMLConstants.XMLNS_ATTRIBUTE.length() + 1);
94                 }
95                 namespaces.put(prefix, attribute.getNodeValue());
96             }
97         }
98
99         // namespace does not have to be defined on this element but inherited
100         if (!namespaces.containsKey(DEFAULT_NAMESPACE_PREFIX)) {
101             Optional<String> namespaceOptionally = getNamespaceOptionally();
102             if (namespaceOptionally.isPresent()) {
103                 namespaces.put(DEFAULT_NAMESPACE_PREFIX, namespaceOptionally.get());
104             }
105         }
106
107         return namespaces;
108     }
109
110     public void checkName(final String expectedName) throws UnexpectedElementException {
111         if (!getName().equals(expectedName)) {
112             throw new UnexpectedElementException(
113                     String.format("Expected %s xml element but was %s", expectedName, getName()),
114                     ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, ErrorSeverity.ERROR);
115         }
116     }
117
118     public void checkNamespaceAttribute(final String expectedNamespace)
119             throws UnexpectedNamespaceException, MissingNameSpaceException {
120         if (!getNamespaceAttribute().equals(expectedNamespace)) {
121             throw new UnexpectedNamespaceException(
122                     String.format("Unexpected namespace %s should be %s", getNamespaceAttribute(), expectedNamespace),
123                     ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, ErrorSeverity.ERROR);
124         }
125     }
126
127     public void checkNamespace(final String expectedNamespace)
128             throws UnexpectedNamespaceException, MissingNameSpaceException {
129         if (!getNamespace().equals(expectedNamespace)) {
130             throw new UnexpectedNamespaceException(
131                     String.format("Unexpected namespace %s should be %s", getNamespace(), expectedNamespace),
132                     ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, ErrorSeverity.ERROR);
133         }
134     }
135
136     public String getName() {
137         final String localName = element.getLocalName();
138         if (!Strings.isNullOrEmpty(localName)) {
139             return localName;
140         }
141         return element.getTagName();
142     }
143
144     public String getAttribute(final String attributeName) {
145         return element.getAttribute(attributeName);
146     }
147
148     public String getAttribute(final String attributeName, final String namespace) {
149         return element.getAttributeNS(namespace, attributeName);
150     }
151
152     public NodeList getElementsByTagName(final String name) {
153         return element.getElementsByTagName(name);
154     }
155
156     public void appendChild(final Element toAppend) {
157         this.element.appendChild(toAppend);
158     }
159
160     public Element getDomElement() {
161         return element;
162     }
163
164     public Map<String, Attr> getAttributes() {
165
166         Map<String, Attr> mappedAttributes = new HashMap<>();
167
168         NamedNodeMap attributes = element.getAttributes();
169         for (int i = 0; i < attributes.getLength(); i++) {
170             Attr attr = (Attr) attributes.item(i);
171             mappedAttributes.put(attr.getNodeName(), attr);
172         }
173
174         return mappedAttributes;
175     }
176
177     /**
178      * Non recursive.
179      */
180     private List<XmlElement> getChildElementsInternal(final ElementFilteringStrategy strat) {
181         NodeList childNodes = element.getChildNodes();
182         final List<XmlElement> result = new ArrayList<>();
183         for (int i = 0; i < childNodes.getLength(); i++) {
184             Node item = childNodes.item(i);
185             if (!(item instanceof Element)) {
186                 continue;
187             }
188             if (strat.accept((Element) item)) {
189                 result.add(new XmlElement((Element) item));
190             }
191         }
192
193         return result;
194     }
195
196     public List<XmlElement> getChildElements() {
197         return getChildElementsInternal(e -> true);
198     }
199
200     /**
201      * Returns the child elements for the given tag.
202      *
203      * @param tagName tag name without prefix
204      * @return List of child elements
205      */
206     public List<XmlElement> getChildElements(final String tagName) {
207         return getChildElementsInternal(e -> e.getLocalName().equals(tagName));
208     }
209
210     public List<XmlElement> getChildElementsWithinNamespace(final String childName, final String namespace) {
211         return Lists.newArrayList(Collections2.filter(getChildElementsWithinNamespace(namespace),
212             xmlElement -> xmlElement.getName().equals(childName)));
213     }
214
215     public List<XmlElement> getChildElementsWithinNamespace(final String namespace) {
216         return getChildElementsInternal(e -> {
217             try {
218                 return XmlElement.fromDomElement(e).getNamespace().equals(namespace);
219             } catch (final MissingNameSpaceException e1) {
220                 return false;
221             }
222         });
223     }
224
225     public Optional<XmlElement> getOnlyChildElementOptionally(final String childName) {
226         List<XmlElement> nameElements = getChildElements(childName);
227         if (nameElements.size() != 1) {
228             return Optional.empty();
229         }
230         return Optional.of(nameElements.get(0));
231     }
232
233     public Optional<XmlElement> getOnlyChildElementOptionally(final String childName, final String namespace) {
234         List<XmlElement> children = getChildElementsWithinNamespace(namespace);
235         children = Lists.newArrayList(Collections2.filter(children,
236             xmlElement -> xmlElement.getName().equals(childName)));
237         if (children.size() != 1) {
238             return Optional.empty();
239         }
240         return Optional.of(children.get(0));
241     }
242
243     public Optional<XmlElement> getOnlyChildElementOptionally() {
244         List<XmlElement> children = getChildElements();
245         if (children.size() != 1) {
246             return Optional.empty();
247         }
248         return Optional.of(children.get(0));
249     }
250
251     public XmlElement getOnlyChildElementWithSameNamespace(final String childName) throws  DocumentedException {
252         return getOnlyChildElement(childName, getNamespace());
253     }
254
255     public XmlElement getOnlyChildElementWithSameNamespace() throws DocumentedException {
256         XmlElement childElement = getOnlyChildElement();
257         childElement.checkNamespace(getNamespace());
258         return childElement;
259     }
260
261     public Optional<XmlElement> getOnlyChildElementWithSameNamespaceOptionally(final String childName) {
262         Optional<String> namespace = getNamespaceOptionally();
263         if (namespace.isPresent()) {
264             List<XmlElement> children = getChildElementsWithinNamespace(namespace.get());
265             children = Lists.newArrayList(Collections2.filter(children,
266                 xmlElement -> xmlElement.getName().equals(childName)));
267             if (children.size() != 1) {
268                 return Optional.empty();
269             }
270             return Optional.of(children.get(0));
271         }
272         return Optional.empty();
273     }
274
275     public Optional<XmlElement> getOnlyChildElementWithSameNamespaceOptionally() {
276         Optional<XmlElement> child = getOnlyChildElementOptionally();
277         if (child.isPresent()
278                 && child.get().getNamespaceOptionally().isPresent()
279                 && getNamespaceOptionally().isPresent()
280                 && getNamespaceOptionally().get().equals(child.get().getNamespaceOptionally().get())) {
281             return child;
282         }
283         return Optional.empty();
284     }
285
286     public XmlElement getOnlyChildElement(final String childName, final String namespace) throws DocumentedException {
287         List<XmlElement> children = getChildElementsWithinNamespace(namespace);
288         children = Lists.newArrayList(Collections2.filter(children,
289             xmlElement -> xmlElement.getName().equals(childName)));
290         if (children.size() != 1) {
291             throw new DocumentedException(String.format("One element %s:%s expected in %s but was %s", namespace,
292                     childName, toString(), children.size()),
293                     ErrorType.APPLICATION, ErrorTag.INVALID_VALUE, ErrorSeverity.ERROR);
294         }
295
296         return children.get(0);
297     }
298
299     public XmlElement getOnlyChildElement(final String childName) throws DocumentedException {
300         List<XmlElement> nameElements = getChildElements(childName);
301         if (nameElements.size() != 1) {
302             throw new DocumentedException("One element " + childName + " expected in " + toString(),
303                     ErrorType.APPLICATION, ErrorTag.INVALID_VALUE, ErrorSeverity.ERROR);
304         }
305         return nameElements.get(0);
306     }
307
308     public XmlElement getOnlyChildElement() throws DocumentedException {
309         List<XmlElement> children = getChildElements();
310         if (children.size() != 1) {
311             throw new DocumentedException(
312                     String.format("One element expected in %s but was %s", toString(), children.size()),
313                     ErrorType.APPLICATION, ErrorTag.INVALID_VALUE, ErrorSeverity.ERROR);
314         }
315         return children.get(0);
316     }
317
318     public String getTextContent() throws DocumentedException {
319         NodeList childNodes = element.getChildNodes();
320         if (childNodes.getLength() == 0) {
321             return DEFAULT_NAMESPACE_PREFIX;
322         }
323         for (int i = 0; i < childNodes.getLength(); i++) {
324             Node textChild = childNodes.item(i);
325             if (textChild instanceof Text) {
326                 String content = textChild.getTextContent();
327                 return content.trim();
328             }
329         }
330         throw new DocumentedException(getName() + " should contain text.",
331                 ErrorType.APPLICATION, ErrorTag.INVALID_VALUE, ErrorSeverity.ERROR);
332     }
333
334     public Optional<String> getOnlyTextContentOptionally() {
335         // only return text content if this node has exactly one Text child node
336         if (element.getChildNodes().getLength() == 1) {
337             Node item = element.getChildNodes().item(0);
338             if (item instanceof Text) {
339                 return Optional.of(((Text) item).getWholeText());
340             }
341         }
342         return Optional.empty();
343     }
344
345     public String getNamespaceAttribute() throws MissingNameSpaceException {
346         String attribute = element.getAttribute(XMLConstants.XMLNS_ATTRIBUTE);
347         if (attribute.isEmpty() || attribute.equals(DEFAULT_NAMESPACE_PREFIX)) {
348             throw new MissingNameSpaceException(String.format("Element %s must specify namespace", toString()),
349                     ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, ErrorSeverity.ERROR);
350         }
351         return attribute;
352     }
353
354     public Optional<String> getNamespaceAttributeOptionally() {
355         String attribute = element.getAttribute(XMLConstants.XMLNS_ATTRIBUTE);
356         if (attribute.isEmpty() || attribute.equals(DEFAULT_NAMESPACE_PREFIX)) {
357             return Optional.empty();
358         }
359         return Optional.of(attribute);
360     }
361
362     public Optional<String> getNamespaceOptionally() {
363         String namespaceURI = element.getNamespaceURI();
364         if (Strings.isNullOrEmpty(namespaceURI)) {
365             return Optional.empty();
366         } else {
367             return Optional.of(namespaceURI);
368         }
369     }
370
371     public String getNamespace() throws MissingNameSpaceException {
372         Optional<String> namespaceURI = getNamespaceOptionally();
373         if (namespaceURI.isEmpty()) {
374             throw new MissingNameSpaceException(String.format("No namespace defined for %s", this),
375                     ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, ErrorSeverity.ERROR);
376         }
377         return namespaceURI.get();
378     }
379
380     @Override
381     public String toString() {
382         final StringBuilder sb = new StringBuilder("XmlElement{");
383         sb.append("name='").append(getName()).append('\'');
384         if (element.getNamespaceURI() != null) {
385             try {
386                 sb.append(", namespace='").append(getNamespace()).append('\'');
387             } catch (final MissingNameSpaceException e) {
388                 LOG.trace("Missing namespace for element.");
389             }
390         }
391         sb.append('}');
392         return sb.toString();
393     }
394
395     /**
396      * Search for element's attributes defining namespaces. Look for the one
397      * namespace that matches prefix of element's text content. E.g.
398      *
399      * <pre>
400      * &lt;type
401      * xmlns:th-java="urn:opendaylight:params:xml:ns:yang:controller:threadpool:impl"&gt;
402      *     th-java:threadfactory-naming&lt;/type&gt;
403      * </pre>
404      *
405      * <p>
406      * returns {"th-java","urn:.."}. If no prefix is matched, then default
407      * namespace is returned with empty string as key. If no default namespace
408      * is found value will be null.
409      */
410     public Map.Entry<String/* prefix */, String/* namespace */> findNamespaceOfTextContent()
411             throws DocumentedException {
412         Map<String, String> namespaces = extractNamespaces();
413         String textContent = getTextContent();
414         int indexOfColon = textContent.indexOf(':');
415         String prefix;
416         if (indexOfColon > -1) {
417             prefix = textContent.substring(0, indexOfColon);
418         } else {
419             prefix = DEFAULT_NAMESPACE_PREFIX;
420         }
421         if (!namespaces.containsKey(prefix)) {
422             throw new IllegalArgumentException("Cannot find namespace for " + XmlUtil.toString(element)
423                 + ". Prefix from content is " + prefix + ". Found namespaces " + namespaces);
424         }
425         return new SimpleImmutableEntry<>(prefix, namespaces.get(prefix));
426     }
427
428     public List<XmlElement> getChildElementsWithSameNamespace(final String childName) throws MissingNameSpaceException {
429         List<XmlElement> children = getChildElementsWithinNamespace(getNamespace());
430         return Lists.newArrayList(Collections2.filter(children, xmlElement -> xmlElement.getName().equals(childName)));
431     }
432
433     public void checkUnrecognisedElements(final List<XmlElement> recognisedElements,
434                                           final XmlElement... additionalRecognisedElements) throws DocumentedException {
435         List<XmlElement> childElements = getChildElements();
436         childElements.removeAll(recognisedElements);
437         for (XmlElement additionalRecognisedElement : additionalRecognisedElements) {
438             childElements.remove(additionalRecognisedElement);
439         }
440         if (!childElements.isEmpty()) {
441             throw new DocumentedException(String.format("Unrecognised elements %s in %s", childElements, this),
442                     ErrorType.APPLICATION, ErrorTag.INVALID_VALUE, ErrorSeverity.ERROR);
443         }
444     }
445
446     public void checkUnrecognisedElements(final XmlElement... additionalRecognisedElements) throws DocumentedException {
447         checkUnrecognisedElements(Collections.emptyList(), additionalRecognisedElements);
448     }
449
450     @Override
451     public boolean equals(final Object obj) {
452         if (this == obj) {
453             return true;
454         }
455         if (obj == null || getClass() != obj.getClass()) {
456             return false;
457         }
458
459         XmlElement that = (XmlElement) obj;
460
461         return element.isEqualNode(that.element);
462
463     }
464
465     @Override
466     public int hashCode() {
467         return element.hashCode();
468     }
469
470     public boolean hasNamespace() {
471         return getNamespaceAttributeOptionally().isPresent() || getNamespaceOptionally().isPresent();
472     }
473
474     private interface ElementFilteringStrategy {
475         boolean accept(Element element);
476     }
477 }