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