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.opendaylight.yangtools.yang.common.ErrorSeverity;
24 import org.slf4j.Logger;
25 import org.slf4j.LoggerFactory;
26 import org.w3c.dom.Attr;
27 import org.w3c.dom.Document;
28 import org.w3c.dom.Element;
29 import org.w3c.dom.NamedNodeMap;
30 import org.w3c.dom.Node;
31 import org.w3c.dom.NodeList;
32 import org.w3c.dom.Text;
33 import org.xml.sax.SAXException;
35 public final class XmlElement {
37 public static final String DEFAULT_NAMESPACE_PREFIX = "";
39 private final Element element;
40 private static final Logger LOG = LoggerFactory.getLogger(XmlElement.class);
42 private XmlElement(final Element element) {
43 this.element = element;
46 public static XmlElement fromDomElement(final Element element) {
47 return new XmlElement(element);
50 public static XmlElement fromDomDocument(final Document xml) {
51 return new XmlElement(xml.getDocumentElement());
54 public static XmlElement fromString(final String str) throws DocumentedException {
56 return new XmlElement(XmlUtil.readXmlToElement(str));
57 } catch (IOException | SAXException e) {
58 throw DocumentedException.wrap(e);
62 public static XmlElement fromDomElementWithExpected(final Element element, final String expectedName)
63 throws DocumentedException {
64 XmlElement xmlElement = XmlElement.fromDomElement(element);
65 xmlElement.checkName(expectedName);
69 public static XmlElement fromDomElementWithExpected(final Element element, final String expectedName,
70 final String expectedNamespace) throws DocumentedException {
71 XmlElement xmlElement = XmlElement.fromDomElementWithExpected(element, expectedName);
72 xmlElement.checkNamespace(expectedNamespace);
76 private Map<String, String> extractNamespaces() throws DocumentedException {
77 Map<String, String> namespaces = new HashMap<>();
78 NamedNodeMap attributes = element.getAttributes();
79 for (int i = 0; i < attributes.getLength(); i++) {
80 Node attribute = attributes.item(i);
81 String attribKey = attribute.getNodeName();
82 if (attribKey.startsWith(XMLConstants.XMLNS_ATTRIBUTE)) {
84 if (attribKey.equals(XMLConstants.XMLNS_ATTRIBUTE)) {
85 prefix = DEFAULT_NAMESPACE_PREFIX;
87 if (!attribKey.startsWith(XMLConstants.XMLNS_ATTRIBUTE + ":")) {
88 throw new DocumentedException("Attribute doesn't start with :",
89 DocumentedException.ErrorType.APPLICATION,
90 DocumentedException.ErrorTag.INVALID_VALUE,
93 prefix = attribKey.substring(XMLConstants.XMLNS_ATTRIBUTE.length() + 1);
95 namespaces.put(prefix, attribute.getNodeValue());
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());
110 public void checkName(final String expectedName) throws UnexpectedElementException {
111 if (!getName().equals(expectedName)) {
112 throw new UnexpectedElementException(String.format("Expected %s xml element but was %s", expectedName,
114 DocumentedException.ErrorType.APPLICATION,
115 DocumentedException.ErrorTag.OPERATION_FAILED,
116 ErrorSeverity.ERROR);
120 public void checkNamespaceAttribute(final String expectedNamespace)
121 throws UnexpectedNamespaceException, MissingNameSpaceException {
122 if (!getNamespaceAttribute().equals(expectedNamespace)) {
123 throw new UnexpectedNamespaceException(String.format("Unexpected namespace %s should be %s",
124 getNamespaceAttribute(),
126 DocumentedException.ErrorType.APPLICATION,
127 DocumentedException.ErrorTag.OPERATION_FAILED,
128 ErrorSeverity.ERROR);
132 public void checkNamespace(final String expectedNamespace)
133 throws UnexpectedNamespaceException, MissingNameSpaceException {
134 if (!getNamespace().equals(expectedNamespace)) {
135 throw new UnexpectedNamespaceException(String.format("Unexpected namespace %s should be %s",
138 DocumentedException.ErrorType.APPLICATION,
139 DocumentedException.ErrorTag.OPERATION_FAILED,
140 ErrorSeverity.ERROR);
144 public String getName() {
145 final String localName = element.getLocalName();
146 if (!Strings.isNullOrEmpty(localName)) {
149 return element.getTagName();
152 public String getAttribute(final String attributeName) {
153 return element.getAttribute(attributeName);
156 public String getAttribute(final String attributeName, final String namespace) {
157 return element.getAttributeNS(namespace, attributeName);
160 public NodeList getElementsByTagName(final String name) {
161 return element.getElementsByTagName(name);
164 public void appendChild(final Element toAppend) {
165 this.element.appendChild(toAppend);
168 public Element getDomElement() {
172 public Map<String, Attr> getAttributes() {
174 Map<String, Attr> mappedAttributes = new HashMap<>();
176 NamedNodeMap attributes = element.getAttributes();
177 for (int i = 0; i < attributes.getLength(); i++) {
178 Attr attr = (Attr) attributes.item(i);
179 mappedAttributes.put(attr.getNodeName(), attr);
182 return mappedAttributes;
188 private List<XmlElement> getChildElementsInternal(final ElementFilteringStrategy strat) {
189 NodeList childNodes = element.getChildNodes();
190 final List<XmlElement> result = new ArrayList<>();
191 for (int i = 0; i < childNodes.getLength(); i++) {
192 Node item = childNodes.item(i);
193 if (!(item instanceof Element)) {
196 if (strat.accept((Element) item)) {
197 result.add(new XmlElement((Element) item));
204 public List<XmlElement> getChildElements() {
205 return getChildElementsInternal(e -> true);
209 * Returns the child elements for the given tag.
211 * @param tagName tag name without prefix
212 * @return List of child elements
214 public List<XmlElement> getChildElements(final String tagName) {
215 return getChildElementsInternal(e -> e.getLocalName().equals(tagName));
218 public List<XmlElement> getChildElementsWithinNamespace(final String childName, final String namespace) {
219 return Lists.newArrayList(Collections2.filter(getChildElementsWithinNamespace(namespace),
220 xmlElement -> xmlElement.getName().equals(childName)));
223 public List<XmlElement> getChildElementsWithinNamespace(final String namespace) {
224 return getChildElementsInternal(e -> {
226 return XmlElement.fromDomElement(e).getNamespace().equals(namespace);
227 } catch (final MissingNameSpaceException e1) {
233 public Optional<XmlElement> getOnlyChildElementOptionally(final String childName) {
234 List<XmlElement> nameElements = getChildElements(childName);
235 if (nameElements.size() != 1) {
236 return Optional.empty();
238 return Optional.of(nameElements.get(0));
241 public Optional<XmlElement> getOnlyChildElementOptionally(final String childName, final String namespace) {
242 List<XmlElement> children = getChildElementsWithinNamespace(namespace);
243 children = Lists.newArrayList(Collections2.filter(children,
244 xmlElement -> xmlElement.getName().equals(childName)));
245 if (children.size() != 1) {
246 return Optional.empty();
248 return Optional.of(children.get(0));
251 public Optional<XmlElement> getOnlyChildElementOptionally() {
252 List<XmlElement> children = getChildElements();
253 if (children.size() != 1) {
254 return Optional.empty();
256 return Optional.of(children.get(0));
259 public XmlElement getOnlyChildElementWithSameNamespace(final String childName) throws DocumentedException {
260 return getOnlyChildElement(childName, getNamespace());
263 public XmlElement getOnlyChildElementWithSameNamespace() throws DocumentedException {
264 XmlElement childElement = getOnlyChildElement();
265 childElement.checkNamespace(getNamespace());
269 public Optional<XmlElement> getOnlyChildElementWithSameNamespaceOptionally(final String childName) {
270 Optional<String> namespace = getNamespaceOptionally();
271 if (namespace.isPresent()) {
272 List<XmlElement> children = getChildElementsWithinNamespace(namespace.get());
273 children = Lists.newArrayList(Collections2.filter(children,
274 xmlElement -> xmlElement.getName().equals(childName)));
275 if (children.size() != 1) {
276 return Optional.empty();
278 return Optional.of(children.get(0));
280 return Optional.empty();
283 public Optional<XmlElement> getOnlyChildElementWithSameNamespaceOptionally() {
284 Optional<XmlElement> child = getOnlyChildElementOptionally();
285 if (child.isPresent()
286 && child.get().getNamespaceOptionally().isPresent()
287 && getNamespaceOptionally().isPresent()
288 && getNamespaceOptionally().get().equals(child.get().getNamespaceOptionally().get())) {
291 return Optional.empty();
294 public XmlElement getOnlyChildElement(final String childName, final String namespace) throws DocumentedException {
295 List<XmlElement> children = getChildElementsWithinNamespace(namespace);
296 children = Lists.newArrayList(Collections2.filter(children,
297 xmlElement -> xmlElement.getName().equals(childName)));
298 if (children.size() != 1) {
299 throw new DocumentedException(String.format("One element %s:%s expected in %s but was %s", namespace,
300 childName, toString(), children.size()),
301 DocumentedException.ErrorType.APPLICATION,
302 DocumentedException.ErrorTag.INVALID_VALUE,
303 ErrorSeverity.ERROR);
306 return children.get(0);
309 public XmlElement getOnlyChildElement(final String childName) throws DocumentedException {
310 List<XmlElement> nameElements = getChildElements(childName);
311 if (nameElements.size() != 1) {
312 throw new DocumentedException("One element " + childName + " expected in " + toString(),
313 DocumentedException.ErrorType.APPLICATION,
314 DocumentedException.ErrorTag.INVALID_VALUE,
315 ErrorSeverity.ERROR);
317 return nameElements.get(0);
320 public XmlElement getOnlyChildElement() throws DocumentedException {
321 List<XmlElement> children = getChildElements();
322 if (children.size() != 1) {
323 throw new DocumentedException(String.format("One element expected in %s but was %s", toString(),
325 DocumentedException.ErrorType.APPLICATION,
326 DocumentedException.ErrorTag.INVALID_VALUE,
327 ErrorSeverity.ERROR);
329 return children.get(0);
332 public String getTextContent() throws DocumentedException {
333 NodeList childNodes = element.getChildNodes();
334 if (childNodes.getLength() == 0) {
335 return DEFAULT_NAMESPACE_PREFIX;
337 for (int i = 0; i < childNodes.getLength(); i++) {
338 Node textChild = childNodes.item(i);
339 if (textChild instanceof Text) {
340 String content = textChild.getTextContent();
341 return content.trim();
344 throw new DocumentedException(getName() + " should contain text.",
345 DocumentedException.ErrorType.APPLICATION,
346 DocumentedException.ErrorTag.INVALID_VALUE,
351 public Optional<String> getOnlyTextContentOptionally() {
352 // only return text content if this node has exactly one Text child node
353 if (element.getChildNodes().getLength() == 1) {
354 Node item = element.getChildNodes().item(0);
355 if (item instanceof Text) {
356 return Optional.of(((Text) item).getWholeText());
359 return Optional.empty();
362 public String getNamespaceAttribute() throws MissingNameSpaceException {
363 String attribute = element.getAttribute(XMLConstants.XMLNS_ATTRIBUTE);
364 if (attribute.isEmpty() || attribute.equals(DEFAULT_NAMESPACE_PREFIX)) {
365 throw new MissingNameSpaceException(String.format("Element %s must specify namespace",
367 DocumentedException.ErrorType.APPLICATION,
368 DocumentedException.ErrorTag.OPERATION_FAILED,
369 ErrorSeverity.ERROR);
374 public Optional<String> getNamespaceAttributeOptionally() {
375 String attribute = element.getAttribute(XMLConstants.XMLNS_ATTRIBUTE);
376 if (attribute.isEmpty() || attribute.equals(DEFAULT_NAMESPACE_PREFIX)) {
377 return Optional.empty();
379 return Optional.of(attribute);
382 public Optional<String> getNamespaceOptionally() {
383 String namespaceURI = element.getNamespaceURI();
384 if (Strings.isNullOrEmpty(namespaceURI)) {
385 return Optional.empty();
387 return Optional.of(namespaceURI);
391 public String getNamespace() throws MissingNameSpaceException {
392 Optional<String> namespaceURI = getNamespaceOptionally();
393 if (namespaceURI.isEmpty()) {
394 throw new MissingNameSpaceException(String.format("No namespace defined for %s", this),
395 DocumentedException.ErrorType.APPLICATION,
396 DocumentedException.ErrorTag.OPERATION_FAILED,
397 ErrorSeverity.ERROR);
399 return namespaceURI.get();
403 public String toString() {
404 final StringBuilder sb = new StringBuilder("XmlElement{");
405 sb.append("name='").append(getName()).append('\'');
406 if (element.getNamespaceURI() != null) {
408 sb.append(", namespace='").append(getNamespace()).append('\'');
409 } catch (final MissingNameSpaceException e) {
410 LOG.trace("Missing namespace for element.");
414 return sb.toString();
418 * Search for element's attributes defining namespaces. Look for the one
419 * namespace that matches prefix of element's text content. E.g.
423 * xmlns:th-java="urn:opendaylight:params:xml:ns:yang:controller:threadpool:impl">
424 * th-java:threadfactory-naming</type>
428 * returns {"th-java","urn:.."}. If no prefix is matched, then default
429 * namespace is returned with empty string as key. If no default namespace
430 * is found value will be null.
432 public Map.Entry<String/* prefix */, String/* namespace */> findNamespaceOfTextContent()
433 throws DocumentedException {
434 Map<String, String> namespaces = extractNamespaces();
435 String textContent = getTextContent();
436 int indexOfColon = textContent.indexOf(':');
438 if (indexOfColon > -1) {
439 prefix = textContent.substring(0, indexOfColon);
441 prefix = DEFAULT_NAMESPACE_PREFIX;
443 if (!namespaces.containsKey(prefix)) {
444 throw new IllegalArgumentException("Cannot find namespace for " + XmlUtil.toString(element)
445 + ". Prefix from content is " + prefix + ". Found namespaces " + namespaces);
447 return new SimpleImmutableEntry<>(prefix, namespaces.get(prefix));
450 public List<XmlElement> getChildElementsWithSameNamespace(final String childName) throws MissingNameSpaceException {
451 List<XmlElement> children = getChildElementsWithinNamespace(getNamespace());
452 return Lists.newArrayList(Collections2.filter(children, xmlElement -> xmlElement.getName().equals(childName)));
455 public void checkUnrecognisedElements(final List<XmlElement> recognisedElements,
456 final XmlElement... additionalRecognisedElements) throws DocumentedException {
457 List<XmlElement> childElements = getChildElements();
458 childElements.removeAll(recognisedElements);
459 for (XmlElement additionalRecognisedElement : additionalRecognisedElements) {
460 childElements.remove(additionalRecognisedElement);
462 if (!childElements.isEmpty()) {
463 throw new DocumentedException(String.format("Unrecognised elements %s in %s", childElements, this),
464 DocumentedException.ErrorType.APPLICATION,
465 DocumentedException.ErrorTag.INVALID_VALUE,
466 ErrorSeverity.ERROR);
470 public void checkUnrecognisedElements(final XmlElement... additionalRecognisedElements) throws DocumentedException {
471 checkUnrecognisedElements(Collections.emptyList(), additionalRecognisedElements);
475 public boolean equals(final Object obj) {
479 if (obj == null || getClass() != obj.getClass()) {
483 XmlElement that = (XmlElement) obj;
485 return element.isEqualNode(that.element);
490 public int hashCode() {
491 return element.hashCode();
494 public boolean hasNamespace() {
495 return getNamespaceAttributeOptionally().isPresent() || getNamespaceOptionally().isPresent();
498 private interface ElementFilteringStrategy {
499 boolean accept(Element element);