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