2 * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved.
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
8 package org.opendaylight.netconf.api.xml;
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;
20 import java.util.Optional;
21 import javax.xml.XMLConstants;
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;
34 public final class XmlElement {
36 public static final String DEFAULT_NAMESPACE_PREFIX = "";
38 private final Element element;
39 private static final Logger LOG = LoggerFactory.getLogger(XmlElement.class);
41 private XmlElement(final Element element) {
42 this.element = element;
45 public static XmlElement fromDomElement(final Element element) {
46 return new XmlElement(element);
49 public static XmlElement fromDomDocument(final Document xml) {
50 return new XmlElement(xml.getDocumentElement());
53 public static XmlElement fromString(final String str) throws DocumentedException {
55 return new XmlElement(XmlUtil.readXmlToElement(str));
56 } catch (IOException | SAXException e) {
57 throw DocumentedException.wrap(e);
61 public static XmlElement fromDomElementWithExpected(final Element element, final String expectedName)
62 throws DocumentedException {
63 XmlElement xmlElement = XmlElement.fromDomElement(element);
64 xmlElement.checkName(expectedName);
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);
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(XMLConstants.XMLNS_ATTRIBUTE)) {
83 if (attribKey.equals(XMLConstants.XMLNS_ATTRIBUTE)) {
84 prefix = DEFAULT_NAMESPACE_PREFIX;
86 if (!attribKey.startsWith(XMLConstants.XMLNS_ATTRIBUTE + ":")) {
87 throw new DocumentedException("Attribute doesn't start with :",
88 DocumentedException.ErrorType.APPLICATION,
89 DocumentedException.ErrorTag.INVALID_VALUE,
90 DocumentedException.ErrorSeverity.ERROR);
92 prefix = attribKey.substring(XMLConstants.XMLNS_ATTRIBUTE.length() + 1);
94 namespaces.put(prefix, attribute.getNodeValue());
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());
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,
113 DocumentedException.ErrorType.APPLICATION,
114 DocumentedException.ErrorTag.OPERATION_FAILED,
115 DocumentedException.ErrorSeverity.ERROR);
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(),
125 DocumentedException.ErrorType.APPLICATION,
126 DocumentedException.ErrorTag.OPERATION_FAILED,
127 DocumentedException.ErrorSeverity.ERROR);
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",
137 DocumentedException.ErrorType.APPLICATION,
138 DocumentedException.ErrorTag.OPERATION_FAILED,
139 DocumentedException.ErrorSeverity.ERROR);
143 public String getName() {
144 final String localName = element.getLocalName();
145 if (!Strings.isNullOrEmpty(localName)) {
148 return element.getTagName();
151 public String getAttribute(final String attributeName) {
152 return element.getAttribute(attributeName);
155 public String getAttribute(final String attributeName, final String namespace) {
156 return element.getAttributeNS(namespace, attributeName);
159 public NodeList getElementsByTagName(final String name) {
160 return element.getElementsByTagName(name);
163 public void appendChild(final Element toAppend) {
164 this.element.appendChild(toAppend);
167 public Element getDomElement() {
171 public Map<String, Attr> getAttributes() {
173 Map<String, Attr> mappedAttributes = new HashMap<>();
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);
181 return mappedAttributes;
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)) {
195 if (strat.accept((Element) item)) {
196 result.add(new XmlElement((Element) item));
203 public List<XmlElement> getChildElements() {
204 return getChildElementsInternal(e -> true);
208 * Returns the child elements for the given tag.
210 * @param tagName tag name without prefix
211 * @return List of child elements
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);
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)));
225 public List<XmlElement> getChildElementsWithinNamespace(final String namespace) {
226 return getChildElementsInternal(e -> {
228 return XmlElement.fromDomElement(e).getNamespace().equals(namespace);
229 } catch (final MissingNameSpaceException e1) {
235 public Optional<XmlElement> getOnlyChildElementOptionally(final String childName) {
236 List<XmlElement> nameElements = getChildElements(childName);
237 if (nameElements.size() != 1) {
238 return Optional.empty();
240 return Optional.of(nameElements.get(0));
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.empty();
250 return Optional.of(children.get(0));
253 public Optional<XmlElement> getOnlyChildElementOptionally() {
254 List<XmlElement> children = getChildElements();
255 if (children.size() != 1) {
256 return Optional.empty();
258 return Optional.of(children.get(0));
261 public XmlElement getOnlyChildElementWithSameNamespace(final String childName) throws DocumentedException {
262 return getOnlyChildElement(childName, getNamespace());
265 public XmlElement getOnlyChildElementWithSameNamespace() throws DocumentedException {
266 XmlElement childElement = getOnlyChildElement();
267 childElement.checkNamespace(getNamespace());
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.empty();
280 return Optional.of(children.get(0));
282 return Optional.empty();
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())) {
293 return Optional.empty();
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);
308 return children.get(0);
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);
319 return nameElements.get(0);
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(),
327 DocumentedException.ErrorType.APPLICATION,
328 DocumentedException.ErrorTag.INVALID_VALUE,
329 DocumentedException.ErrorSeverity.ERROR);
331 return children.get(0);
334 public String getTextContent() throws DocumentedException {
335 NodeList childNodes = element.getChildNodes();
336 if (childNodes.getLength() == 0) {
337 return DEFAULT_NAMESPACE_PREFIX;
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();
346 throw new DocumentedException(getName() + " should contain text.",
347 DocumentedException.ErrorType.APPLICATION,
348 DocumentedException.ErrorTag.INVALID_VALUE,
349 DocumentedException.ErrorSeverity.ERROR
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());
361 return Optional.empty();
364 public String getNamespaceAttribute() throws MissingNameSpaceException {
365 String attribute = element.getAttribute(XMLConstants.XMLNS_ATTRIBUTE);
366 if (attribute.isEmpty() || attribute.equals(DEFAULT_NAMESPACE_PREFIX)) {
367 throw new MissingNameSpaceException(String.format("Element %s must specify namespace",
369 DocumentedException.ErrorType.APPLICATION,
370 DocumentedException.ErrorTag.OPERATION_FAILED,
371 DocumentedException.ErrorSeverity.ERROR);
376 public Optional<String> getNamespaceAttributeOptionally() {
377 String attribute = element.getAttribute(XMLConstants.XMLNS_ATTRIBUTE);
378 if (attribute.isEmpty() || attribute.equals(DEFAULT_NAMESPACE_PREFIX)) {
379 return Optional.empty();
381 return Optional.of(attribute);
384 public Optional<String> getNamespaceOptionally() {
385 String namespaceURI = element.getNamespaceURI();
386 if (Strings.isNullOrEmpty(namespaceURI)) {
387 return Optional.empty();
389 return Optional.of(namespaceURI);
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);
401 return namespaceURI.get();
405 public String toString() {
406 final StringBuilder sb = new StringBuilder("XmlElement{");
407 sb.append("name='").append(getName()).append('\'');
408 if (element.getNamespaceURI() != null) {
410 sb.append(", namespace='").append(getNamespace()).append('\'');
411 } catch (final MissingNameSpaceException e) {
412 LOG.trace("Missing namespace for element.");
416 return sb.toString();
420 * Search for element's attributes defining namespaces. Look for the one
421 * namespace that matches prefix of element's text content. E.g.
425 * xmlns:th-java="urn:opendaylight:params:xml:ns:yang:controller:threadpool:impl">
426 * th-java:threadfactory-naming</type>
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.
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(':');
440 if (indexOfColon > -1) {
441 prefix = textContent.substring(0, indexOfColon);
443 prefix = DEFAULT_NAMESPACE_PREFIX;
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);
449 return new SimpleImmutableEntry<>(prefix, namespaces.get(prefix));
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)));
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);
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);
472 public void checkUnrecognisedElements(final XmlElement... additionalRecognisedElements) throws DocumentedException {
473 checkUnrecognisedElements(Collections.emptyList(), additionalRecognisedElements);
477 public boolean equals(final Object obj) {
481 if (obj == null || getClass() != obj.getClass()) {
485 XmlElement that = (XmlElement) obj;
487 return element.isEqualNode(that.element);
492 public int hashCode() {
493 return element.hashCode();
496 public boolean hasNamespace() {
497 return getNamespaceAttributeOptionally().isPresent() || getNamespaceOptionally().isPresent();
500 private interface ElementFilteringStrategy {
501 boolean accept(Element element);