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.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;
37 public final class XmlElement {
39 public static final String DEFAULT_NAMESPACE_PREFIX = "";
41 private final Element element;
42 private static final Logger LOG = LoggerFactory.getLogger(XmlElement.class);
44 private XmlElement(final Element element) {
45 this.element = element;
48 public static XmlElement fromDomElement(final Element element) {
49 return new XmlElement(element);
52 public static XmlElement fromDomDocument(final Document xml) {
53 return new XmlElement(xml.getDocumentElement());
56 public static XmlElement fromString(final String str) throws DocumentedException {
58 return new XmlElement(XmlUtil.readXmlToElement(str));
59 } catch (IOException | SAXException e) {
60 throw DocumentedException.wrap(e);
64 public static XmlElement fromDomElementWithExpected(final Element element, final String expectedName)
65 throws DocumentedException {
66 XmlElement xmlElement = XmlElement.fromDomElement(element);
67 xmlElement.checkName(expectedName);
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);
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)) {
86 if (attribKey.equals(XMLConstants.XMLNS_ATTRIBUTE)) {
87 prefix = DEFAULT_NAMESPACE_PREFIX;
89 if (!attribKey.startsWith(XMLConstants.XMLNS_ATTRIBUTE + ":")) {
90 throw new DocumentedException("Attribute doesn't start with :",
91 ErrorType.APPLICATION, ErrorTag.INVALID_VALUE, ErrorSeverity.ERROR);
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(
113 String.format("Expected %s xml element but was %s", expectedName, getName()),
114 ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, ErrorSeverity.ERROR);
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);
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);
136 public String getName() {
137 final String localName = element.getLocalName();
138 if (!Strings.isNullOrEmpty(localName)) {
141 return element.getTagName();
144 public String getAttribute(final String attributeName) {
145 return element.getAttribute(attributeName);
148 public String getAttribute(final String attributeName, final String namespace) {
149 return element.getAttributeNS(namespace, attributeName);
152 public NodeList getElementsByTagName(final String name) {
153 return element.getElementsByTagName(name);
156 public void appendChild(final Element toAppend) {
157 element.appendChild(toAppend);
160 public Element getDomElement() {
164 public Map<String, Attr> getAttributes() {
166 Map<String, Attr> mappedAttributes = new HashMap<>();
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);
174 return mappedAttributes;
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));
192 public List<XmlElement> getChildElements() {
193 return getChildElementsInternal(e -> true);
197 * Returns the child elements for the given tag.
199 * @param tagName tag name without prefix
200 * @return List of child elements
202 public List<XmlElement> getChildElements(final String tagName) {
203 return getChildElementsInternal(e -> e.getLocalName().equals(tagName));
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)));
211 public List<XmlElement> getChildElementsWithinNamespace(final String namespace) {
212 return getChildElementsInternal(e -> {
214 return XmlElement.fromDomElement(e).getNamespace().equals(namespace);
215 } catch (final MissingNameSpaceException e1) {
221 public Optional<XmlElement> getOnlyChildElementOptionally(final String childName) {
222 List<XmlElement> nameElements = getChildElements(childName);
223 if (nameElements.size() != 1) {
224 return Optional.empty();
226 return Optional.of(nameElements.get(0));
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();
236 return Optional.of(children.get(0));
239 public Optional<XmlElement> getOnlyChildElementOptionally() {
240 List<XmlElement> children = getChildElements();
241 if (children.size() != 1) {
242 return Optional.empty();
244 return Optional.of(children.get(0));
247 public XmlElement getOnlyChildElementWithSameNamespace(final String childName) throws DocumentedException {
248 return getOnlyChildElement(childName, getNamespace());
251 public XmlElement getOnlyChildElementWithSameNamespace() throws DocumentedException {
252 XmlElement childElement = getOnlyChildElement();
253 childElement.checkNamespace(getNamespace());
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();
266 return Optional.of(children.get(0));
268 return Optional.empty();
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())) {
279 return Optional.empty();
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);
292 return children.get(0);
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);
301 return nameElements.get(0);
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);
311 return children.get(0);
314 public String getTextContent() throws DocumentedException {
315 NodeList childNodes = element.getChildNodes();
316 if (childNodes.getLength() == 0) {
317 return DEFAULT_NAMESPACE_PREFIX;
319 for (int i = 0; i < childNodes.getLength(); i++) {
320 if (childNodes.item(i) instanceof Text textChild) {
321 return textChild.getTextContent().trim();
324 throw new DocumentedException(getName() + " should contain text.",
325 ErrorType.APPLICATION, ErrorTag.INVALID_VALUE, ErrorSeverity.ERROR);
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());
335 return Optional.empty();
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);
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();
352 return Optional.of(attribute);
355 public Optional<String> getNamespaceOptionally() {
356 String namespaceURI = element.getNamespaceURI();
357 if (Strings.isNullOrEmpty(namespaceURI)) {
358 return Optional.empty();
360 return Optional.of(namespaceURI);
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);
370 return namespaceURI.get();
374 public String toString() {
375 final StringBuilder sb = new StringBuilder("XmlElement{");
376 sb.append("name='").append(getName()).append('\'');
377 if (element.getNamespaceURI() != null) {
379 sb.append(", namespace='").append(getNamespace()).append('\'');
380 } catch (final MissingNameSpaceException e) {
381 LOG.trace("Missing namespace for element.");
385 return sb.toString();
389 * Search for element's attributes defining namespaces. Look for the one
390 * namespace that matches prefix of element's text content. E.g.
394 * xmlns:th-java="urn:opendaylight:params:xml:ns:yang:controller:threadpool:impl">
395 * th-java:threadfactory-naming</type>
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.
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(':');
409 if (indexOfColon > -1) {
410 prefix = textContent.substring(0, indexOfColon);
412 prefix = DEFAULT_NAMESPACE_PREFIX;
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);
418 return new SimpleImmutableEntry<>(prefix, namespaces.get(prefix));
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)));
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);
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);
439 public void checkUnrecognisedElements(final XmlElement... additionalRecognisedElements) throws DocumentedException {
440 checkUnrecognisedElements(Collections.emptyList(), additionalRecognisedElements);
444 public boolean equals(final Object obj) {
448 if (obj == null || getClass() != obj.getClass()) {
452 XmlElement that = (XmlElement) obj;
454 return element.isEqualNode(that.element);
459 public int hashCode() {
460 return element.hashCode();
463 public boolean hasNamespace() {
464 return getNamespaceAttributeOptionally().isPresent() || getNamespaceOptionally().isPresent();
467 private interface ElementFilteringStrategy {
468 boolean accept(Element element);