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