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