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.ErrorType;
25 import org.slf4j.Logger;
26 import org.slf4j.LoggerFactory;
27 import org.w3c.dom.Attr;
28 import org.w3c.dom.Document;
29 import org.w3c.dom.Element;
30 import org.w3c.dom.NamedNodeMap;
31 import org.w3c.dom.Node;
32 import org.w3c.dom.NodeList;
33 import org.w3c.dom.Text;
34 import org.xml.sax.SAXException;
36 public final class XmlElement {
38 public static final String DEFAULT_NAMESPACE_PREFIX = "";
40 private final Element element;
41 private static final Logger LOG = LoggerFactory.getLogger(XmlElement.class);
43 private XmlElement(final Element element) {
44 this.element = element;
47 public static XmlElement fromDomElement(final Element element) {
48 return new XmlElement(element);
51 public static XmlElement fromDomDocument(final Document xml) {
52 return new XmlElement(xml.getDocumentElement());
55 public static XmlElement fromString(final String str) throws DocumentedException {
57 return new XmlElement(XmlUtil.readXmlToElement(str));
58 } catch (IOException | SAXException e) {
59 throw DocumentedException.wrap(e);
63 public static XmlElement fromDomElementWithExpected(final Element element, final String expectedName)
64 throws DocumentedException {
65 XmlElement xmlElement = XmlElement.fromDomElement(element);
66 xmlElement.checkName(expectedName);
70 public static XmlElement fromDomElementWithExpected(final Element element, final String expectedName,
71 final String expectedNamespace) throws DocumentedException {
72 XmlElement xmlElement = XmlElement.fromDomElementWithExpected(element, expectedName);
73 xmlElement.checkNamespace(expectedNamespace);
77 private Map<String, String> extractNamespaces() throws DocumentedException {
78 Map<String, String> namespaces = new HashMap<>();
79 NamedNodeMap attributes = element.getAttributes();
80 for (int i = 0; i < attributes.getLength(); i++) {
81 Node attribute = attributes.item(i);
82 String attribKey = attribute.getNodeName();
83 if (attribKey.startsWith(XMLConstants.XMLNS_ATTRIBUTE)) {
85 if (attribKey.equals(XMLConstants.XMLNS_ATTRIBUTE)) {
86 prefix = DEFAULT_NAMESPACE_PREFIX;
88 if (!attribKey.startsWith(XMLConstants.XMLNS_ATTRIBUTE + ":")) {
89 throw new DocumentedException("Attribute doesn't start with :",
90 ErrorType.APPLICATION, DocumentedException.ErrorTag.INVALID_VALUE, 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 ErrorType.APPLICATION, DocumentedException.ErrorTag.OPERATION_FAILED, ErrorSeverity.ERROR);
117 public void checkNamespaceAttribute(final String expectedNamespace)
118 throws UnexpectedNamespaceException, MissingNameSpaceException {
119 if (!getNamespaceAttribute().equals(expectedNamespace)) {
120 throw new UnexpectedNamespaceException(
121 String.format("Unexpected namespace %s should be %s", getNamespaceAttribute(), expectedNamespace),
122 ErrorType.APPLICATION, DocumentedException.ErrorTag.OPERATION_FAILED, ErrorSeverity.ERROR);
126 public void checkNamespace(final String expectedNamespace)
127 throws UnexpectedNamespaceException, MissingNameSpaceException {
128 if (!getNamespace().equals(expectedNamespace)) {
129 throw new UnexpectedNamespaceException(
130 String.format("Unexpected namespace %s should be %s", getNamespace(), expectedNamespace),
131 ErrorType.APPLICATION, DocumentedException.ErrorTag.OPERATION_FAILED, ErrorSeverity.ERROR);
135 public String getName() {
136 final String localName = element.getLocalName();
137 if (!Strings.isNullOrEmpty(localName)) {
140 return element.getTagName();
143 public String getAttribute(final String attributeName) {
144 return element.getAttribute(attributeName);
147 public String getAttribute(final String attributeName, final String namespace) {
148 return element.getAttributeNS(namespace, attributeName);
151 public NodeList getElementsByTagName(final String name) {
152 return element.getElementsByTagName(name);
155 public void appendChild(final Element toAppend) {
156 this.element.appendChild(toAppend);
159 public Element getDomElement() {
163 public Map<String, Attr> getAttributes() {
165 Map<String, Attr> mappedAttributes = new HashMap<>();
167 NamedNodeMap attributes = element.getAttributes();
168 for (int i = 0; i < attributes.getLength(); i++) {
169 Attr attr = (Attr) attributes.item(i);
170 mappedAttributes.put(attr.getNodeName(), attr);
173 return mappedAttributes;
179 private List<XmlElement> getChildElementsInternal(final ElementFilteringStrategy strat) {
180 NodeList childNodes = element.getChildNodes();
181 final List<XmlElement> result = new ArrayList<>();
182 for (int i = 0; i < childNodes.getLength(); i++) {
183 Node item = childNodes.item(i);
184 if (!(item instanceof Element)) {
187 if (strat.accept((Element) item)) {
188 result.add(new XmlElement((Element) item));
195 public List<XmlElement> getChildElements() {
196 return getChildElementsInternal(e -> true);
200 * Returns the child elements for the given tag.
202 * @param tagName tag name without prefix
203 * @return List of child elements
205 public List<XmlElement> getChildElements(final String tagName) {
206 return getChildElementsInternal(e -> e.getLocalName().equals(tagName));
209 public List<XmlElement> getChildElementsWithinNamespace(final String childName, final String namespace) {
210 return Lists.newArrayList(Collections2.filter(getChildElementsWithinNamespace(namespace),
211 xmlElement -> xmlElement.getName().equals(childName)));
214 public List<XmlElement> getChildElementsWithinNamespace(final String namespace) {
215 return getChildElementsInternal(e -> {
217 return XmlElement.fromDomElement(e).getNamespace().equals(namespace);
218 } catch (final MissingNameSpaceException e1) {
224 public Optional<XmlElement> getOnlyChildElementOptionally(final String childName) {
225 List<XmlElement> nameElements = getChildElements(childName);
226 if (nameElements.size() != 1) {
227 return Optional.empty();
229 return Optional.of(nameElements.get(0));
232 public Optional<XmlElement> getOnlyChildElementOptionally(final String childName, final String namespace) {
233 List<XmlElement> children = getChildElementsWithinNamespace(namespace);
234 children = Lists.newArrayList(Collections2.filter(children,
235 xmlElement -> xmlElement.getName().equals(childName)));
236 if (children.size() != 1) {
237 return Optional.empty();
239 return Optional.of(children.get(0));
242 public Optional<XmlElement> getOnlyChildElementOptionally() {
243 List<XmlElement> children = getChildElements();
244 if (children.size() != 1) {
245 return Optional.empty();
247 return Optional.of(children.get(0));
250 public XmlElement getOnlyChildElementWithSameNamespace(final String childName) throws DocumentedException {
251 return getOnlyChildElement(childName, getNamespace());
254 public XmlElement getOnlyChildElementWithSameNamespace() throws DocumentedException {
255 XmlElement childElement = getOnlyChildElement();
256 childElement.checkNamespace(getNamespace());
260 public Optional<XmlElement> getOnlyChildElementWithSameNamespaceOptionally(final String childName) {
261 Optional<String> namespace = getNamespaceOptionally();
262 if (namespace.isPresent()) {
263 List<XmlElement> children = getChildElementsWithinNamespace(namespace.get());
264 children = Lists.newArrayList(Collections2.filter(children,
265 xmlElement -> xmlElement.getName().equals(childName)));
266 if (children.size() != 1) {
267 return Optional.empty();
269 return Optional.of(children.get(0));
271 return Optional.empty();
274 public Optional<XmlElement> getOnlyChildElementWithSameNamespaceOptionally() {
275 Optional<XmlElement> child = getOnlyChildElementOptionally();
276 if (child.isPresent()
277 && child.get().getNamespaceOptionally().isPresent()
278 && getNamespaceOptionally().isPresent()
279 && getNamespaceOptionally().get().equals(child.get().getNamespaceOptionally().get())) {
282 return Optional.empty();
285 public XmlElement getOnlyChildElement(final String childName, final String namespace) throws DocumentedException {
286 List<XmlElement> children = getChildElementsWithinNamespace(namespace);
287 children = Lists.newArrayList(Collections2.filter(children,
288 xmlElement -> xmlElement.getName().equals(childName)));
289 if (children.size() != 1) {
290 throw new DocumentedException(String.format("One element %s:%s expected in %s but was %s", namespace,
291 childName, toString(), children.size()),
292 ErrorType.APPLICATION, DocumentedException.ErrorTag.INVALID_VALUE, ErrorSeverity.ERROR);
295 return children.get(0);
298 public XmlElement getOnlyChildElement(final String childName) throws DocumentedException {
299 List<XmlElement> nameElements = getChildElements(childName);
300 if (nameElements.size() != 1) {
301 throw new DocumentedException("One element " + childName + " expected in " + toString(),
302 ErrorType.APPLICATION, DocumentedException.ErrorTag.INVALID_VALUE, ErrorSeverity.ERROR);
304 return nameElements.get(0);
307 public XmlElement getOnlyChildElement() throws DocumentedException {
308 List<XmlElement> children = getChildElements();
309 if (children.size() != 1) {
310 throw new DocumentedException(String.format("One element expected in %s but was %s", toString(),
312 ErrorType.APPLICATION, DocumentedException.ErrorTag.INVALID_VALUE, ErrorSeverity.ERROR);
314 return children.get(0);
317 public String getTextContent() throws DocumentedException {
318 NodeList childNodes = element.getChildNodes();
319 if (childNodes.getLength() == 0) {
320 return DEFAULT_NAMESPACE_PREFIX;
322 for (int i = 0; i < childNodes.getLength(); i++) {
323 Node textChild = childNodes.item(i);
324 if (textChild instanceof Text) {
325 String content = textChild.getTextContent();
326 return content.trim();
329 throw new DocumentedException(getName() + " should contain text.",
330 ErrorType.APPLICATION, DocumentedException.ErrorTag.INVALID_VALUE, ErrorSeverity.ERROR);
333 public Optional<String> getOnlyTextContentOptionally() {
334 // only return text content if this node has exactly one Text child node
335 if (element.getChildNodes().getLength() == 1) {
336 Node item = element.getChildNodes().item(0);
337 if (item instanceof Text) {
338 return Optional.of(((Text) item).getWholeText());
341 return Optional.empty();
344 public String getNamespaceAttribute() throws MissingNameSpaceException {
345 String attribute = element.getAttribute(XMLConstants.XMLNS_ATTRIBUTE);
346 if (attribute.isEmpty() || attribute.equals(DEFAULT_NAMESPACE_PREFIX)) {
347 throw new MissingNameSpaceException(String.format("Element %s must specify namespace", toString()),
348 ErrorType.APPLICATION, DocumentedException.ErrorTag.OPERATION_FAILED, ErrorSeverity.ERROR);
353 public Optional<String> getNamespaceAttributeOptionally() {
354 String attribute = element.getAttribute(XMLConstants.XMLNS_ATTRIBUTE);
355 if (attribute.isEmpty() || attribute.equals(DEFAULT_NAMESPACE_PREFIX)) {
356 return Optional.empty();
358 return Optional.of(attribute);
361 public Optional<String> getNamespaceOptionally() {
362 String namespaceURI = element.getNamespaceURI();
363 if (Strings.isNullOrEmpty(namespaceURI)) {
364 return Optional.empty();
366 return Optional.of(namespaceURI);
370 public String getNamespace() throws MissingNameSpaceException {
371 Optional<String> namespaceURI = getNamespaceOptionally();
372 if (namespaceURI.isEmpty()) {
373 throw new MissingNameSpaceException(String.format("No namespace defined for %s", this),
374 ErrorType.APPLICATION, DocumentedException.ErrorTag.OPERATION_FAILED, ErrorSeverity.ERROR);
376 return namespaceURI.get();
380 public String toString() {
381 final StringBuilder sb = new StringBuilder("XmlElement{");
382 sb.append("name='").append(getName()).append('\'');
383 if (element.getNamespaceURI() != null) {
385 sb.append(", namespace='").append(getNamespace()).append('\'');
386 } catch (final MissingNameSpaceException e) {
387 LOG.trace("Missing namespace for element.");
391 return sb.toString();
395 * Search for element's attributes defining namespaces. Look for the one
396 * namespace that matches prefix of element's text content. E.g.
400 * xmlns:th-java="urn:opendaylight:params:xml:ns:yang:controller:threadpool:impl">
401 * th-java:threadfactory-naming</type>
405 * returns {"th-java","urn:.."}. If no prefix is matched, then default
406 * namespace is returned with empty string as key. If no default namespace
407 * is found value will be null.
409 public Map.Entry<String/* prefix */, String/* namespace */> findNamespaceOfTextContent()
410 throws DocumentedException {
411 Map<String, String> namespaces = extractNamespaces();
412 String textContent = getTextContent();
413 int indexOfColon = textContent.indexOf(':');
415 if (indexOfColon > -1) {
416 prefix = textContent.substring(0, indexOfColon);
418 prefix = DEFAULT_NAMESPACE_PREFIX;
420 if (!namespaces.containsKey(prefix)) {
421 throw new IllegalArgumentException("Cannot find namespace for " + XmlUtil.toString(element)
422 + ". Prefix from content is " + prefix + ". Found namespaces " + namespaces);
424 return new SimpleImmutableEntry<>(prefix, namespaces.get(prefix));
427 public List<XmlElement> getChildElementsWithSameNamespace(final String childName) throws MissingNameSpaceException {
428 List<XmlElement> children = getChildElementsWithinNamespace(getNamespace());
429 return Lists.newArrayList(Collections2.filter(children, xmlElement -> xmlElement.getName().equals(childName)));
432 public void checkUnrecognisedElements(final List<XmlElement> recognisedElements,
433 final XmlElement... additionalRecognisedElements) throws DocumentedException {
434 List<XmlElement> childElements = getChildElements();
435 childElements.removeAll(recognisedElements);
436 for (XmlElement additionalRecognisedElement : additionalRecognisedElements) {
437 childElements.remove(additionalRecognisedElement);
439 if (!childElements.isEmpty()) {
440 throw new DocumentedException(String.format("Unrecognised elements %s in %s", childElements, this),
441 ErrorType.APPLICATION, DocumentedException.ErrorTag.INVALID_VALUE, ErrorSeverity.ERROR);
445 public void checkUnrecognisedElements(final XmlElement... additionalRecognisedElements) throws DocumentedException {
446 checkUnrecognisedElements(Collections.emptyList(), additionalRecognisedElements);
450 public boolean equals(final Object obj) {
454 if (obj == null || getClass() != obj.getClass()) {
458 XmlElement that = (XmlElement) obj;
460 return element.isEqualNode(that.element);
465 public int hashCode() {
466 return element.hashCode();
469 public boolean hasNamespace() {
470 return getNamespaceAttributeOptionally().isPresent() || getNamespaceOptionally().isPresent();
473 private interface ElementFilteringStrategy {
474 boolean accept(Element element);