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