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