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