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