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