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
9 package org.opendaylight.controller.config.util.xml;
11 import com.google.common.base.Optional;
12 import com.google.common.base.Predicate;
13 import com.google.common.base.Strings;
14 import com.google.common.collect.Collections2;
15 import com.google.common.collect.Lists;
16 import com.google.common.collect.Maps;
17 import java.io.IOException;
18 import java.util.ArrayList;
19 import java.util.Collections;
20 import java.util.HashMap;
21 import java.util.List;
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(Element element) {
42 this.element = element;
45 public static XmlElement fromDomElement(Element e) {
46 return new XmlElement(e);
49 public static XmlElement fromDomDocument(Document xml) {
50 return new XmlElement(xml.getDocumentElement());
53 public static XmlElement fromString(String s) throws DocumentedException {
55 return new XmlElement(XmlUtil.readXmlToElement(s));
56 } catch (IOException | SAXException e) {
57 throw DocumentedException.wrap(e);
61 public static XmlElement fromDomElementWithExpected(Element element, String expectedName) throws DocumentedException {
62 XmlElement xmlElement = XmlElement.fromDomElement(element);
63 xmlElement.checkName(expectedName);
67 public static XmlElement fromDomElementWithExpected(Element element, String expectedName, String expectedNamespace) throws DocumentedException {
68 XmlElement xmlElement = XmlElement.fromDomElementWithExpected(element, expectedName);
69 xmlElement.checkNamespace(expectedNamespace);
73 private Map<String, String> extractNamespaces() throws DocumentedException {
74 Map<String, String> namespaces = new HashMap<>();
75 NamedNodeMap attributes = element.getAttributes();
76 for (int i = 0; i < attributes.getLength(); i++) {
77 Node attribute = attributes.item(i);
78 String attribKey = attribute.getNodeName();
79 if (attribKey.startsWith(XmlUtil.XMLNS_ATTRIBUTE_KEY)) {
81 if (attribKey.equals(XmlUtil.XMLNS_ATTRIBUTE_KEY)) {
82 prefix = DEFAULT_NAMESPACE_PREFIX;
84 if (!attribKey.startsWith(XmlUtil.XMLNS_ATTRIBUTE_KEY + ":")){
85 throw new DocumentedException("Attribute doesn't start with :",
86 DocumentedException.ErrorType.APPLICATION,
87 DocumentedException.ErrorTag.INVALID_VALUE,
88 DocumentedException.ErrorSeverity.ERROR);
90 prefix = attribKey.substring(XmlUtil.XMLNS_ATTRIBUTE_KEY.length() + 1);
92 namespaces.put(prefix, attribute.getNodeValue());
96 // namespace does not have to be defined on this element but inherited
97 if(!namespaces.containsKey(DEFAULT_NAMESPACE_PREFIX)) {
98 Optional<String> namespaceOptionally = getNamespaceOptionally();
99 if(namespaceOptionally.isPresent()) {
100 namespaces.put(DEFAULT_NAMESPACE_PREFIX, namespaceOptionally.get());
107 public void checkName(String expectedName) throws UnexpectedElementException {
108 if (!getName().equals(expectedName)){
109 throw new UnexpectedElementException(String.format("Expected %s xml element but was %s", expectedName,
111 DocumentedException.ErrorType.APPLICATION,
112 DocumentedException.ErrorTag.OPERATION_FAILED,
113 DocumentedException.ErrorSeverity.ERROR);
117 public void checkNamespaceAttribute(String expectedNamespace) throws UnexpectedNamespaceException, MissingNameSpaceException {
118 if (!getNamespaceAttribute().equals(expectedNamespace))
120 throw new UnexpectedNamespaceException(String.format("Unexpected namespace %s should be %s",
121 getNamespaceAttribute(),
123 DocumentedException.ErrorType.APPLICATION,
124 DocumentedException.ErrorTag.OPERATION_FAILED,
125 DocumentedException.ErrorSeverity.ERROR);
129 public void checkNamespace(String expectedNamespace) throws UnexpectedNamespaceException, MissingNameSpaceException {
130 if (!getNamespace().equals(expectedNamespace))
132 throw new UnexpectedNamespaceException(String.format("Unexpected namespace %s should be %s",
135 DocumentedException.ErrorType.APPLICATION,
136 DocumentedException.ErrorTag.OPERATION_FAILED,
137 DocumentedException.ErrorSeverity.ERROR);
141 public String getName() {
142 final String localName = element.getLocalName();
143 if (!Strings.isNullOrEmpty(localName)){
146 return element.getTagName();
149 public String getAttribute(String attributeName) {
150 return element.getAttribute(attributeName);
153 public String getAttribute(String attributeName, String namespace) {
154 return element.getAttributeNS(namespace, attributeName);
157 public NodeList getElementsByTagName(String name) {
158 return element.getElementsByTagName(name);
161 public void appendChild(Element element) {
162 this.element.appendChild(element);
165 public Element getDomElement() {
169 public Map<String, Attr> getAttributes() {
171 Map<String, Attr> mappedAttributes = Maps.newHashMap();
173 NamedNodeMap attributes = element.getAttributes();
174 for (int i = 0; i < attributes.getLength(); i++) {
175 Attr attr = (Attr) attributes.item(i);
176 mappedAttributes.put(attr.getNodeName(), attr);
179 return mappedAttributes;
185 private List<XmlElement> getChildElementsInternal(ElementFilteringStrategy strat) {
186 NodeList childNodes = element.getChildNodes();
187 final List<XmlElement> result = new ArrayList<>();
188 for (int i = 0; i < childNodes.getLength(); i++) {
189 Node item = childNodes.item(i);
190 if (!(item instanceof Element)) {
193 if (strat.accept((Element) item)) {
194 result.add(new XmlElement((Element) item));
201 public List<XmlElement> getChildElements() {
202 return getChildElementsInternal(new ElementFilteringStrategy() {
204 public boolean accept(Element e) {
210 public List<XmlElement> getChildElementsWithinNamespace(final String childName, String namespace) {
211 return Lists.newArrayList(Collections2.filter(getChildElementsWithinNamespace(namespace),
212 new Predicate<XmlElement>() {
214 public boolean apply(XmlElement xmlElement) {
215 return xmlElement.getName().equals(childName);
220 public List<XmlElement> getChildElementsWithinNamespace(final String namespace) {
221 return getChildElementsInternal(new ElementFilteringStrategy() {
223 public boolean accept(Element e) {
225 return XmlElement.fromDomElement(e).getNamespace().equals(namespace);
226 } catch (MissingNameSpaceException e1) {
236 * @param tagName tag name without prefix
237 * @return List of child elements
239 public List<XmlElement> getChildElements(final String tagName) {
240 return getChildElementsInternal(new ElementFilteringStrategy() {
242 public boolean accept(Element e) {
243 // localName returns pure localName without prefix
244 return e.getLocalName().equals(tagName);
249 public XmlElement getOnlyChildElement(String childName) throws DocumentedException {
250 List<XmlElement> nameElements = getChildElements(childName);
251 if (nameElements.size() != 1){
252 throw new DocumentedException("One element " + childName + " expected in " + toString(),
253 DocumentedException.ErrorType.APPLICATION,
254 DocumentedException.ErrorTag.INVALID_VALUE,
255 DocumentedException.ErrorSeverity.ERROR);
257 return nameElements.get(0);
260 public Optional<XmlElement> getOnlyChildElementOptionally(String childName) {
261 List<XmlElement> nameElements = getChildElements(childName);
262 if (nameElements.size() != 1) {
263 return Optional.absent();
265 return Optional.of(nameElements.get(0));
268 public Optional<XmlElement> getOnlyChildElementOptionally(final String childName, final String namespace) {
269 List<XmlElement> children = getChildElementsWithinNamespace(namespace);
270 children = Lists.newArrayList(Collections2.filter(children, new Predicate<XmlElement>() {
272 public boolean apply(XmlElement xmlElement) {
273 return xmlElement.getName().equals(childName);
276 if (children.size() != 1){
277 return Optional.absent();
279 return Optional.of(children.get(0));
282 public XmlElement getOnlyChildElementWithSameNamespace(String childName) throws DocumentedException {
283 return getOnlyChildElement(childName, getNamespace());
286 public Optional<XmlElement> getOnlyChildElementWithSameNamespaceOptionally(final String childName) {
287 Optional<String> namespace = getNamespaceOptionally();
288 if (namespace.isPresent()) {
289 List<XmlElement> children = getChildElementsWithinNamespace(namespace.get());
290 children = Lists.newArrayList(Collections2.filter(children, new Predicate<XmlElement>() {
292 public boolean apply(XmlElement xmlElement) {
293 return xmlElement.getName().equals(childName);
296 if (children.size() != 1){
297 return Optional.absent();
299 return Optional.of(children.get(0));
301 return Optional.absent();
304 public XmlElement getOnlyChildElementWithSameNamespace() throws DocumentedException {
305 XmlElement childElement = getOnlyChildElement();
306 childElement.checkNamespace(getNamespace());
310 public Optional<XmlElement> getOnlyChildElementWithSameNamespaceOptionally() {
311 Optional<XmlElement> child = getOnlyChildElementOptionally();
312 if (child.isPresent()
313 && child.get().getNamespaceOptionally().isPresent()
314 && getNamespaceOptionally().isPresent()
315 && getNamespaceOptionally().get().equals(child.get().getNamespaceOptionally().get())) {
318 return Optional.absent();
321 public XmlElement getOnlyChildElement(final String childName, String namespace) throws DocumentedException {
322 List<XmlElement> children = getChildElementsWithinNamespace(namespace);
323 children = Lists.newArrayList(Collections2.filter(children, new Predicate<XmlElement>() {
325 public boolean apply(XmlElement xmlElement) {
326 return xmlElement.getName().equals(childName);
329 if (children.size() != 1){
330 throw new DocumentedException(String.format("One element %s:%s expected in %s but was %s", namespace,
331 childName, toString(), children.size()),
332 DocumentedException.ErrorType.APPLICATION,
333 DocumentedException.ErrorTag.INVALID_VALUE,
334 DocumentedException.ErrorSeverity.ERROR);
337 return children.get(0);
340 public XmlElement getOnlyChildElement() throws DocumentedException {
341 List<XmlElement> children = getChildElements();
342 if (children.size() != 1){
343 throw new DocumentedException(String.format( "One element expected in %s but was %s", toString(),
345 DocumentedException.ErrorType.APPLICATION,
346 DocumentedException.ErrorTag.INVALID_VALUE,
347 DocumentedException.ErrorSeverity.ERROR);
349 return children.get(0);
352 public Optional<XmlElement> getOnlyChildElementOptionally() {
353 List<XmlElement> children = getChildElements();
354 if (children.size() != 1) {
355 return Optional.absent();
357 return Optional.of(children.get(0));
360 public String getTextContent() throws DocumentedException {
361 NodeList childNodes = element.getChildNodes();
362 if (childNodes.getLength() == 0) {
363 return DEFAULT_NAMESPACE_PREFIX;
365 for(int i = 0; i < childNodes.getLength(); i++) {
366 Node textChild = childNodes.item(i);
367 if (textChild instanceof Text) {
368 String content = textChild.getTextContent();
369 return content.trim();
372 throw new DocumentedException(getName() + " should contain text.",
373 DocumentedException.ErrorType.APPLICATION,
374 DocumentedException.ErrorTag.INVALID_VALUE,
375 DocumentedException.ErrorSeverity.ERROR
379 public Optional<String> getOnlyTextContentOptionally() {
380 // only return text content if this node has exactly one Text child node
381 if (element.getChildNodes().getLength() == 1) {
382 Node item = element.getChildNodes().item(0);
383 if (item instanceof Text) {
384 return Optional.of(((Text) item).getWholeText());
387 return Optional.absent();
390 public String getNamespaceAttribute() throws MissingNameSpaceException {
391 String attribute = element.getAttribute(XmlUtil.XMLNS_ATTRIBUTE_KEY);
392 if (attribute == null || attribute.equals(DEFAULT_NAMESPACE_PREFIX)){
393 throw new MissingNameSpaceException(String.format("Element %s must specify namespace",
395 DocumentedException.ErrorType.APPLICATION,
396 DocumentedException.ErrorTag.OPERATION_FAILED,
397 DocumentedException.ErrorSeverity.ERROR);
402 public Optional<String> getNamespaceAttributeOptionally(){
403 String attribute = element.getAttribute(XmlUtil.XMLNS_ATTRIBUTE_KEY);
404 if (attribute == null || attribute.equals(DEFAULT_NAMESPACE_PREFIX)){
405 return Optional.absent();
407 return Optional.of(attribute);
410 public Optional<String> getNamespaceOptionally() {
411 String namespaceURI = element.getNamespaceURI();
412 if (Strings.isNullOrEmpty(namespaceURI)) {
413 return Optional.absent();
415 return Optional.of(namespaceURI);
419 public String getNamespace() throws MissingNameSpaceException {
420 Optional<String> namespaceURI = getNamespaceOptionally();
421 if (!namespaceURI.isPresent()){
422 throw new MissingNameSpaceException(String.format("No namespace defined for %s", this),
423 DocumentedException.ErrorType.APPLICATION,
424 DocumentedException.ErrorTag.OPERATION_FAILED,
425 DocumentedException.ErrorSeverity.ERROR);
427 return namespaceURI.get();
431 public String toString() {
432 final StringBuilder sb = new StringBuilder("XmlElement{");
433 sb.append("name='").append(getName()).append('\'');
434 if (element.getNamespaceURI() != null) {
436 sb.append(", namespace='").append(getNamespace()).append('\'');
437 } catch (MissingNameSpaceException e) {
438 LOG.trace("Missing namespace for element.");
442 return sb.toString();
446 * Search for element's attributes defining namespaces. Look for the one
447 * namespace that matches prefix of element's text content. E.g.
451 * xmlns:th-java="urn:opendaylight:params:xml:ns:yang:controller:threadpool:impl">th-java:threadfactory-naming</type>
454 * returns {"th-java","urn:.."}. If no prefix is matched, then default
455 * namespace is returned with empty string as key. If no default namespace
456 * is found value will be null.
458 public Map.Entry<String/* prefix */, String/* namespace */> findNamespaceOfTextContent() throws DocumentedException {
459 Map<String, String> namespaces = extractNamespaces();
460 String textContent = getTextContent();
461 int indexOfColon = textContent.indexOf(':');
463 if (indexOfColon > -1) {
464 prefix = textContent.substring(0, indexOfColon);
466 prefix = DEFAULT_NAMESPACE_PREFIX;
468 if (!namespaces.containsKey(prefix)) {
469 throw new IllegalArgumentException("Cannot find namespace for " + XmlUtil.toString(element) + ". Prefix from content is "
470 + prefix + ". Found namespaces " + namespaces);
472 return Maps.immutableEntry(prefix, namespaces.get(prefix));
475 public List<XmlElement> getChildElementsWithSameNamespace(final String childName) throws MissingNameSpaceException {
476 List<XmlElement> children = getChildElementsWithinNamespace(getNamespace());
477 return Lists.newArrayList(Collections2.filter(children, new Predicate<XmlElement>() {
479 public boolean apply(XmlElement xmlElement) {
480 return xmlElement.getName().equals(childName);
485 public void checkUnrecognisedElements(List<XmlElement> recognisedElements,
486 XmlElement... additionalRecognisedElements) throws DocumentedException {
487 List<XmlElement> childElements = getChildElements();
488 childElements.removeAll(recognisedElements);
489 for (XmlElement additionalRecognisedElement : additionalRecognisedElements) {
490 childElements.remove(additionalRecognisedElement);
492 if (!childElements.isEmpty()){
493 throw new DocumentedException(String.format("Unrecognised elements %s in %s", childElements, this),
494 DocumentedException.ErrorType.APPLICATION,
495 DocumentedException.ErrorTag.INVALID_VALUE,
496 DocumentedException.ErrorSeverity.ERROR);
500 public void checkUnrecognisedElements(XmlElement... additionalRecognisedElements) throws DocumentedException {
501 checkUnrecognisedElements(Collections.<XmlElement>emptyList(), additionalRecognisedElements);
505 public boolean equals(Object o) {
509 if (o == null || getClass() != o.getClass()) {
513 XmlElement that = (XmlElement) o;
515 return element.isEqualNode(that.element);
520 public int hashCode() {
521 return element.hashCode();
524 public boolean hasNamespace() {
525 return getNamespaceAttributeOptionally().isPresent() || getNamespaceOptionally().isPresent();
528 private interface ElementFilteringStrategy {
529 boolean accept(Element e);