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