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 org.opendaylight.netconf.api.DocumentedException;
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;
33 public final class XmlElement {
35 public static final String DEFAULT_NAMESPACE_PREFIX = "";
37 private final Element element;
38 private static final Logger LOG = LoggerFactory.getLogger(XmlElement.class);
40 private XmlElement(final Element element) {
41 this.element = element;
44 public static XmlElement fromDomElement(final Element element) {
45 return new XmlElement(element);
48 public static XmlElement fromDomDocument(final Document xml) {
49 return new XmlElement(xml.getDocumentElement());
52 public static XmlElement fromString(final String str) throws DocumentedException {
54 return new XmlElement(XmlUtil.readXmlToElement(str));
55 } catch (IOException | SAXException e) {
56 throw DocumentedException.wrap(e);
60 public static XmlElement fromDomElementWithExpected(final Element element, final String expectedName)
61 throws DocumentedException {
62 XmlElement xmlElement = XmlElement.fromDomElement(element);
63 xmlElement.checkName(expectedName);
67 public static XmlElement fromDomElementWithExpected(final Element element, final String expectedName,
68 final String expectedNamespace) throws DocumentedException {
69 XmlElement xmlElement = XmlElement.fromDomElementWithExpected(element, expectedName);
70 xmlElement.checkNamespace(expectedNamespace);
74 private Map<String, String> extractNamespaces() throws DocumentedException {
75 Map<String, String> namespaces = new HashMap<>();
76 NamedNodeMap attributes = element.getAttributes();
77 for (int i = 0; i < attributes.getLength(); i++) {
78 Node attribute = attributes.item(i);
79 String attribKey = attribute.getNodeName();
80 if (attribKey.startsWith(XmlUtil.XMLNS_ATTRIBUTE_KEY)) {
82 if (attribKey.equals(XmlUtil.XMLNS_ATTRIBUTE_KEY)) {
83 prefix = DEFAULT_NAMESPACE_PREFIX;
85 if (!attribKey.startsWith(XmlUtil.XMLNS_ATTRIBUTE_KEY + ":")) {
86 throw new DocumentedException("Attribute doesn't start with :",
87 DocumentedException.ErrorType.APPLICATION,
88 DocumentedException.ErrorTag.INVALID_VALUE,
89 DocumentedException.ErrorSeverity.ERROR);
91 prefix = attribKey.substring(XmlUtil.XMLNS_ATTRIBUTE_KEY.length() + 1);
93 namespaces.put(prefix, attribute.getNodeValue());
97 // namespace does not have to be defined on this element but inherited
98 if (!namespaces.containsKey(DEFAULT_NAMESPACE_PREFIX)) {
99 Optional<String> namespaceOptionally = getNamespaceOptionally();
100 if (namespaceOptionally.isPresent()) {
101 namespaces.put(DEFAULT_NAMESPACE_PREFIX, namespaceOptionally.get());
108 public void checkName(final String expectedName) throws UnexpectedElementException {
109 if (!getName().equals(expectedName)) {
110 throw new UnexpectedElementException(String.format("Expected %s xml element but was %s", expectedName,
112 DocumentedException.ErrorType.APPLICATION,
113 DocumentedException.ErrorTag.OPERATION_FAILED,
114 DocumentedException.ErrorSeverity.ERROR);
118 public void checkNamespaceAttribute(final String expectedNamespace)
119 throws UnexpectedNamespaceException, MissingNameSpaceException {
120 if (!getNamespaceAttribute().equals(expectedNamespace)) {
121 throw new UnexpectedNamespaceException(String.format("Unexpected namespace %s should be %s",
122 getNamespaceAttribute(),
124 DocumentedException.ErrorType.APPLICATION,
125 DocumentedException.ErrorTag.OPERATION_FAILED,
126 DocumentedException.ErrorSeverity.ERROR);
130 public void checkNamespace(final String expectedNamespace)
131 throws UnexpectedNamespaceException, MissingNameSpaceException {
132 if (!getNamespace().equals(expectedNamespace)) {
133 throw new UnexpectedNamespaceException(String.format("Unexpected namespace %s should be %s",
136 DocumentedException.ErrorType.APPLICATION,
137 DocumentedException.ErrorTag.OPERATION_FAILED,
138 DocumentedException.ErrorSeverity.ERROR);
142 public String getName() {
143 final String localName = element.getLocalName();
144 if (!Strings.isNullOrEmpty(localName)) {
147 return element.getTagName();
150 public String getAttribute(final String attributeName) {
151 return element.getAttribute(attributeName);
154 public String getAttribute(final String attributeName, final String namespace) {
155 return element.getAttributeNS(namespace, attributeName);
158 public NodeList getElementsByTagName(final String name) {
159 return element.getElementsByTagName(name);
162 public void appendChild(final Element toAppend) {
163 this.element.appendChild(toAppend);
166 public Element getDomElement() {
170 public Map<String, Attr> getAttributes() {
172 Map<String, Attr> mappedAttributes = new HashMap<>();
174 NamedNodeMap attributes = element.getAttributes();
175 for (int i = 0; i < attributes.getLength(); i++) {
176 Attr attr = (Attr) attributes.item(i);
177 mappedAttributes.put(attr.getNodeName(), attr);
180 return mappedAttributes;
186 private List<XmlElement> getChildElementsInternal(final ElementFilteringStrategy strat) {
187 NodeList childNodes = element.getChildNodes();
188 final List<XmlElement> result = new ArrayList<>();
189 for (int i = 0; i < childNodes.getLength(); i++) {
190 Node item = childNodes.item(i);
191 if (!(item instanceof Element)) {
194 if (strat.accept((Element) item)) {
195 result.add(new XmlElement((Element) item));
202 public List<XmlElement> getChildElements() {
203 return getChildElementsInternal(e -> true);
207 * Returns the child elements for the given tag.
209 * @param tagName tag name without prefix
210 * @return List of child elements
212 public List<XmlElement> getChildElements(final String tagName) {
213 return getChildElementsInternal(e -> {
214 // localName returns pure localName without prefix
215 return e.getLocalName().equals(tagName);
219 public List<XmlElement> getChildElementsWithinNamespace(final String childName, final String namespace) {
220 return Lists.newArrayList(Collections2.filter(getChildElementsWithinNamespace(namespace),
221 xmlElement -> xmlElement.getName().equals(childName)));
224 public List<XmlElement> getChildElementsWithinNamespace(final String namespace) {
225 return getChildElementsInternal(e -> {
227 return XmlElement.fromDomElement(e).getNamespace().equals(namespace);
228 } catch (final MissingNameSpaceException e1) {
234 public Optional<XmlElement> getOnlyChildElementOptionally(final String childName) {
235 List<XmlElement> nameElements = getChildElements(childName);
236 if (nameElements.size() != 1) {
237 return Optional.empty();
239 return Optional.of(nameElements.get(0));
242 public Optional<XmlElement> getOnlyChildElementOptionally(final String childName, final String namespace) {
243 List<XmlElement> children = getChildElementsWithinNamespace(namespace);
244 children = Lists.newArrayList(Collections2.filter(children,
245 xmlElement -> xmlElement.getName().equals(childName)));
246 if (children.size() != 1) {
247 return Optional.empty();
249 return Optional.of(children.get(0));
252 public Optional<XmlElement> getOnlyChildElementOptionally() {
253 List<XmlElement> children = getChildElements();
254 if (children.size() != 1) {
255 return Optional.empty();
257 return Optional.of(children.get(0));
260 public XmlElement getOnlyChildElementWithSameNamespace(final String childName) throws DocumentedException {
261 return getOnlyChildElement(childName, getNamespace());
264 public XmlElement getOnlyChildElementWithSameNamespace() throws DocumentedException {
265 XmlElement childElement = getOnlyChildElement();
266 childElement.checkNamespace(getNamespace());
270 public Optional<XmlElement> getOnlyChildElementWithSameNamespaceOptionally(final String childName) {
271 Optional<String> namespace = getNamespaceOptionally();
272 if (namespace.isPresent()) {
273 List<XmlElement> children = getChildElementsWithinNamespace(namespace.get());
274 children = Lists.newArrayList(Collections2.filter(children,
275 xmlElement -> xmlElement.getName().equals(childName)));
276 if (children.size() != 1) {
277 return Optional.empty();
279 return Optional.of(children.get(0));
281 return Optional.empty();
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())) {
292 return Optional.empty();
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);
307 return children.get(0);
310 public XmlElement getOnlyChildElement(final String childName) throws DocumentedException {
311 List<XmlElement> nameElements = getChildElements(childName);
312 if (nameElements.size() != 1) {
313 throw new DocumentedException("One element " + childName + " expected in " + toString(),
314 DocumentedException.ErrorType.APPLICATION,
315 DocumentedException.ErrorTag.INVALID_VALUE,
316 DocumentedException.ErrorSeverity.ERROR);
318 return nameElements.get(0);
321 public XmlElement getOnlyChildElement() throws DocumentedException {
322 List<XmlElement> children = getChildElements();
323 if (children.size() != 1) {
324 throw new DocumentedException(String.format("One element expected in %s but was %s", toString(),
326 DocumentedException.ErrorType.APPLICATION,
327 DocumentedException.ErrorTag.INVALID_VALUE,
328 DocumentedException.ErrorSeverity.ERROR);
330 return children.get(0);
333 public String getTextContent() throws DocumentedException {
334 NodeList childNodes = element.getChildNodes();
335 if (childNodes.getLength() == 0) {
336 return DEFAULT_NAMESPACE_PREFIX;
338 for (int i = 0; i < childNodes.getLength(); i++) {
339 Node textChild = childNodes.item(i);
340 if (textChild instanceof Text) {
341 String content = textChild.getTextContent();
342 return content.trim();
345 throw new DocumentedException(getName() + " should contain text.",
346 DocumentedException.ErrorType.APPLICATION,
347 DocumentedException.ErrorTag.INVALID_VALUE,
348 DocumentedException.ErrorSeverity.ERROR
352 public Optional<String> getOnlyTextContentOptionally() {
353 // only return text content if this node has exactly one Text child node
354 if (element.getChildNodes().getLength() == 1) {
355 Node item = element.getChildNodes().item(0);
356 if (item instanceof Text) {
357 return Optional.of(((Text) item).getWholeText());
360 return Optional.empty();
363 public String getNamespaceAttribute() throws MissingNameSpaceException {
364 String attribute = element.getAttribute(XmlUtil.XMLNS_ATTRIBUTE_KEY);
365 if (attribute.isEmpty() || attribute.equals(DEFAULT_NAMESPACE_PREFIX)) {
366 throw new MissingNameSpaceException(String.format("Element %s must specify namespace",
368 DocumentedException.ErrorType.APPLICATION,
369 DocumentedException.ErrorTag.OPERATION_FAILED,
370 DocumentedException.ErrorSeverity.ERROR);
375 public Optional<String> getNamespaceAttributeOptionally() {
376 String attribute = element.getAttribute(XmlUtil.XMLNS_ATTRIBUTE_KEY);
377 if (attribute.isEmpty() || attribute.equals(DEFAULT_NAMESPACE_PREFIX)) {
378 return Optional.empty();
380 return Optional.of(attribute);
383 public Optional<String> getNamespaceOptionally() {
384 String namespaceURI = element.getNamespaceURI();
385 if (Strings.isNullOrEmpty(namespaceURI)) {
386 return Optional.empty();
388 return Optional.of(namespaceURI);
392 public String getNamespace() throws MissingNameSpaceException {
393 Optional<String> namespaceURI = getNamespaceOptionally();
394 if (!namespaceURI.isPresent()) {
395 throw new MissingNameSpaceException(String.format("No namespace defined for %s", this),
396 DocumentedException.ErrorType.APPLICATION,
397 DocumentedException.ErrorTag.OPERATION_FAILED,
398 DocumentedException.ErrorSeverity.ERROR);
400 return namespaceURI.get();
404 public String toString() {
405 final StringBuilder sb = new StringBuilder("XmlElement{");
406 sb.append("name='").append(getName()).append('\'');
407 if (element.getNamespaceURI() != null) {
409 sb.append(", namespace='").append(getNamespace()).append('\'');
410 } catch (final MissingNameSpaceException e) {
411 LOG.trace("Missing namespace for element.");
415 return sb.toString();
419 * Search for element's attributes defining namespaces. Look for the one
420 * namespace that matches prefix of element's text content. E.g.
424 * xmlns:th-java="urn:opendaylight:params:xml:ns:yang:controller:threadpool:impl">
425 * th-java:threadfactory-naming</type>
429 * returns {"th-java","urn:.."}. If no prefix is matched, then default
430 * namespace is returned with empty string as key. If no default namespace
431 * is found value will be null.
433 public Map.Entry<String/* prefix */, String/* namespace */> findNamespaceOfTextContent()
434 throws DocumentedException {
435 Map<String, String> namespaces = extractNamespaces();
436 String textContent = getTextContent();
437 int indexOfColon = textContent.indexOf(':');
439 if (indexOfColon > -1) {
440 prefix = textContent.substring(0, indexOfColon);
442 prefix = DEFAULT_NAMESPACE_PREFIX;
444 if (!namespaces.containsKey(prefix)) {
445 throw new IllegalArgumentException("Cannot find namespace for " + XmlUtil.toString(element)
446 + ". Prefix from content is " + prefix + ". Found namespaces " + namespaces);
448 return new SimpleImmutableEntry<>(prefix, namespaces.get(prefix));
451 public List<XmlElement> getChildElementsWithSameNamespace(final String childName) throws MissingNameSpaceException {
452 List<XmlElement> children = getChildElementsWithinNamespace(getNamespace());
453 return Lists.newArrayList(Collections2.filter(children, xmlElement -> xmlElement.getName().equals(childName)));
456 public void checkUnrecognisedElements(final List<XmlElement> recognisedElements,
457 final XmlElement... additionalRecognisedElements) throws DocumentedException {
458 List<XmlElement> childElements = getChildElements();
459 childElements.removeAll(recognisedElements);
460 for (XmlElement additionalRecognisedElement : additionalRecognisedElements) {
461 childElements.remove(additionalRecognisedElement);
463 if (!childElements.isEmpty()) {
464 throw new DocumentedException(String.format("Unrecognised elements %s in %s", childElements, this),
465 DocumentedException.ErrorType.APPLICATION,
466 DocumentedException.ErrorTag.INVALID_VALUE,
467 DocumentedException.ErrorSeverity.ERROR);
471 public void checkUnrecognisedElements(final XmlElement... additionalRecognisedElements) throws DocumentedException {
472 checkUnrecognisedElements(Collections.emptyList(), additionalRecognisedElements);
476 public boolean equals(final Object obj) {
480 if (obj == null || getClass() != obj.getClass()) {
484 XmlElement that = (XmlElement) obj;
486 return element.isEqualNode(that.element);
491 public int hashCode() {
492 return element.hashCode();
495 public boolean hasNamespace() {
496 return getNamespaceAttributeOptionally().isPresent() || getNamespaceOptionally().isPresent();
499 private interface ElementFilteringStrategy {
500 boolean accept(Element element);