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.Strings;
13 import com.google.common.collect.Collections2;
14 import com.google.common.collect.Lists;
15 import com.google.common.collect.Maps;
16 import java.io.IOException;
17 import java.util.ArrayList;
18 import java.util.Collections;
19 import java.util.HashMap;
20 import java.util.List;
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 e) {
45 return new XmlElement(e);
48 public static XmlElement fromDomDocument(final Document xml) {
49 return new XmlElement(xml.getDocumentElement());
52 public static XmlElement fromString(final String s) throws DocumentedException {
54 return new XmlElement(XmlUtil.readXmlToElement(s));
55 } catch (IOException | SAXException e) {
56 throw DocumentedException.wrap(e);
60 public static XmlElement fromDomElementWithExpected(final Element element, final String expectedName) throws DocumentedException {
61 XmlElement xmlElement = XmlElement.fromDomElement(element);
62 xmlElement.checkName(expectedName);
66 public static XmlElement fromDomElementWithExpected(final Element element, final String expectedName, final String expectedNamespace) throws DocumentedException {
67 XmlElement xmlElement = XmlElement.fromDomElementWithExpected(element, expectedName);
68 xmlElement.checkNamespace(expectedNamespace);
72 private Map<String, String> extractNamespaces() throws DocumentedException {
73 Map<String, String> namespaces = new HashMap<>();
74 NamedNodeMap attributes = element.getAttributes();
75 for (int i = 0; i < attributes.getLength(); i++) {
76 Node attribute = attributes.item(i);
77 String attribKey = attribute.getNodeName();
78 if (attribKey.startsWith(XmlUtil.XMLNS_ATTRIBUTE_KEY)) {
80 if (attribKey.equals(XmlUtil.XMLNS_ATTRIBUTE_KEY)) {
81 prefix = DEFAULT_NAMESPACE_PREFIX;
83 if (!attribKey.startsWith(XmlUtil.XMLNS_ATTRIBUTE_KEY + ":")){
84 throw new DocumentedException("Attribute doesn't start with :",
85 DocumentedException.ErrorType.APPLICATION,
86 DocumentedException.ErrorTag.INVALID_VALUE,
87 DocumentedException.ErrorSeverity.ERROR);
89 prefix = attribKey.substring(XmlUtil.XMLNS_ATTRIBUTE_KEY.length() + 1);
91 namespaces.put(prefix, attribute.getNodeValue());
95 // namespace does not have to be defined on this element but inherited
96 if(!namespaces.containsKey(DEFAULT_NAMESPACE_PREFIX)) {
97 Optional<String> namespaceOptionally = getNamespaceOptionally();
98 if(namespaceOptionally.isPresent()) {
99 namespaces.put(DEFAULT_NAMESPACE_PREFIX, namespaceOptionally.get());
106 public void checkName(final String expectedName) throws UnexpectedElementException {
107 if (!getName().equals(expectedName)){
108 throw new UnexpectedElementException(String.format("Expected %s xml element but was %s", expectedName,
110 DocumentedException.ErrorType.APPLICATION,
111 DocumentedException.ErrorTag.OPERATION_FAILED,
112 DocumentedException.ErrorSeverity.ERROR);
116 public void checkNamespaceAttribute(final String expectedNamespace) throws UnexpectedNamespaceException, MissingNameSpaceException {
117 if (!getNamespaceAttribute().equals(expectedNamespace))
119 throw new UnexpectedNamespaceException(String.format("Unexpected namespace %s should be %s",
120 getNamespaceAttribute(),
122 DocumentedException.ErrorType.APPLICATION,
123 DocumentedException.ErrorTag.OPERATION_FAILED,
124 DocumentedException.ErrorSeverity.ERROR);
128 public void checkNamespace(final String expectedNamespace) throws UnexpectedNamespaceException, MissingNameSpaceException {
129 if (!getNamespace().equals(expectedNamespace))
131 throw new UnexpectedNamespaceException(String.format("Unexpected namespace %s should be %s",
134 DocumentedException.ErrorType.APPLICATION,
135 DocumentedException.ErrorTag.OPERATION_FAILED,
136 DocumentedException.ErrorSeverity.ERROR);
140 public String getName() {
141 final String localName = element.getLocalName();
142 if (!Strings.isNullOrEmpty(localName)){
145 return element.getTagName();
148 public String getAttribute(final String attributeName) {
149 return element.getAttribute(attributeName);
152 public String getAttribute(final String attributeName, final String namespace) {
153 return element.getAttributeNS(namespace, attributeName);
156 public NodeList getElementsByTagName(final String name) {
157 return element.getElementsByTagName(name);
160 public void appendChild(final Element element) {
161 this.element.appendChild(element);
164 public Element getDomElement() {
168 public Map<String, Attr> getAttributes() {
170 Map<String, Attr> mappedAttributes = Maps.newHashMap();
172 NamedNodeMap attributes = element.getAttributes();
173 for (int i = 0; i < attributes.getLength(); i++) {
174 Attr attr = (Attr) attributes.item(i);
175 mappedAttributes.put(attr.getNodeName(), attr);
178 return mappedAttributes;
184 private List<XmlElement> getChildElementsInternal(final ElementFilteringStrategy strat) {
185 NodeList childNodes = element.getChildNodes();
186 final List<XmlElement> result = new ArrayList<>();
187 for (int i = 0; i < childNodes.getLength(); i++) {
188 Node item = childNodes.item(i);
189 if (!(item instanceof Element)) {
192 if (strat.accept((Element) item)) {
193 result.add(new XmlElement((Element) item));
200 public List<XmlElement> getChildElements() {
201 return getChildElementsInternal(e -> true);
204 public List<XmlElement> getChildElementsWithinNamespace(final String childName, final String namespace) {
205 return Lists.newArrayList(Collections2.filter(getChildElementsWithinNamespace(namespace),
206 xmlElement -> xmlElement.getName().equals(childName)));
209 public List<XmlElement> getChildElementsWithinNamespace(final String namespace) {
210 return getChildElementsInternal(e -> {
212 return XmlElement.fromDomElement(e).getNamespace().equals(namespace);
213 } catch (final MissingNameSpaceException e1) {
221 * @param tagName tag name without prefix
222 * @return List of child elements
224 public List<XmlElement> getChildElements(final String tagName) {
225 return getChildElementsInternal(e -> {
226 // localName returns pure localName without prefix
227 return e.getLocalName().equals(tagName);
231 public XmlElement getOnlyChildElement(final String childName) throws DocumentedException {
232 List<XmlElement> nameElements = getChildElements(childName);
233 if (nameElements.size() != 1){
234 throw new DocumentedException("One element " + childName + " expected in " + toString(),
235 DocumentedException.ErrorType.APPLICATION,
236 DocumentedException.ErrorTag.INVALID_VALUE,
237 DocumentedException.ErrorSeverity.ERROR);
239 return nameElements.get(0);
242 public Optional<XmlElement> getOnlyChildElementOptionally(final String childName) {
243 List<XmlElement> nameElements = getChildElements(childName);
244 if (nameElements.size() != 1) {
245 return Optional.absent();
247 return Optional.of(nameElements.get(0));
250 public Optional<XmlElement> getOnlyChildElementOptionally(final String childName, final String namespace) {
251 List<XmlElement> children = getChildElementsWithinNamespace(namespace);
252 children = Lists.newArrayList(Collections2.filter(children,
253 xmlElement -> xmlElement.getName().equals(childName)));
254 if (children.size() != 1){
255 return Optional.absent();
257 return Optional.of(children.get(0));
260 public XmlElement getOnlyChildElementWithSameNamespace(final String childName) throws DocumentedException {
261 return getOnlyChildElement(childName, getNamespace());
264 public Optional<XmlElement> getOnlyChildElementWithSameNamespaceOptionally(final String childName) {
265 Optional<String> namespace = getNamespaceOptionally();
266 if (namespace.isPresent()) {
267 List<XmlElement> children = getChildElementsWithinNamespace(namespace.get());
268 children = Lists.newArrayList(Collections2.filter(children,
269 xmlElement -> xmlElement.getName().equals(childName)));
270 if (children.size() != 1){
271 return Optional.absent();
273 return Optional.of(children.get(0));
275 return Optional.absent();
278 public XmlElement getOnlyChildElementWithSameNamespace() throws DocumentedException {
279 XmlElement childElement = getOnlyChildElement();
280 childElement.checkNamespace(getNamespace());
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.absent();
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() throws DocumentedException {
311 List<XmlElement> children = getChildElements();
312 if (children.size() != 1){
313 throw new DocumentedException(String.format( "One element expected in %s but was %s", toString(),
315 DocumentedException.ErrorType.APPLICATION,
316 DocumentedException.ErrorTag.INVALID_VALUE,
317 DocumentedException.ErrorSeverity.ERROR);
319 return children.get(0);
322 public Optional<XmlElement> getOnlyChildElementOptionally() {
323 List<XmlElement> children = getChildElements();
324 if (children.size() != 1) {
325 return Optional.absent();
327 return Optional.of(children.get(0));
330 public String getTextContent() throws DocumentedException {
331 NodeList childNodes = element.getChildNodes();
332 if (childNodes.getLength() == 0) {
333 return DEFAULT_NAMESPACE_PREFIX;
335 for(int i = 0; i < childNodes.getLength(); i++) {
336 Node textChild = childNodes.item(i);
337 if (textChild instanceof Text) {
338 String content = textChild.getTextContent();
339 return content.trim();
342 throw new DocumentedException(getName() + " should contain text.",
343 DocumentedException.ErrorType.APPLICATION,
344 DocumentedException.ErrorTag.INVALID_VALUE,
345 DocumentedException.ErrorSeverity.ERROR
349 public Optional<String> getOnlyTextContentOptionally() {
350 // only return text content if this node has exactly one Text child node
351 if (element.getChildNodes().getLength() == 1) {
352 Node item = element.getChildNodes().item(0);
353 if (item instanceof Text) {
354 return Optional.of(((Text) item).getWholeText());
357 return Optional.absent();
360 public String getNamespaceAttribute() throws MissingNameSpaceException {
361 String attribute = element.getAttribute(XmlUtil.XMLNS_ATTRIBUTE_KEY);
362 if (attribute == null || attribute.equals(DEFAULT_NAMESPACE_PREFIX)){
363 throw new MissingNameSpaceException(String.format("Element %s must specify namespace",
365 DocumentedException.ErrorType.APPLICATION,
366 DocumentedException.ErrorTag.OPERATION_FAILED,
367 DocumentedException.ErrorSeverity.ERROR);
372 public Optional<String> getNamespaceAttributeOptionally(){
373 String attribute = element.getAttribute(XmlUtil.XMLNS_ATTRIBUTE_KEY);
374 if (attribute == null || attribute.equals(DEFAULT_NAMESPACE_PREFIX)){
375 return Optional.absent();
377 return Optional.of(attribute);
380 public Optional<String> getNamespaceOptionally() {
381 String namespaceURI = element.getNamespaceURI();
382 if (Strings.isNullOrEmpty(namespaceURI)) {
383 return Optional.absent();
385 return Optional.of(namespaceURI);
389 public String getNamespace() throws MissingNameSpaceException {
390 Optional<String> namespaceURI = getNamespaceOptionally();
391 if (!namespaceURI.isPresent()){
392 throw new MissingNameSpaceException(String.format("No namespace defined for %s", this),
393 DocumentedException.ErrorType.APPLICATION,
394 DocumentedException.ErrorTag.OPERATION_FAILED,
395 DocumentedException.ErrorSeverity.ERROR);
397 return namespaceURI.get();
401 public String toString() {
402 final StringBuilder sb = new StringBuilder("XmlElement{");
403 sb.append("name='").append(getName()).append('\'');
404 if (element.getNamespaceURI() != null) {
406 sb.append(", namespace='").append(getNamespace()).append('\'');
407 } catch (final MissingNameSpaceException e) {
408 LOG.trace("Missing namespace for element.");
412 return sb.toString();
416 * Search for element's attributes defining namespaces. Look for the one
417 * namespace that matches prefix of element's text content. E.g.
421 * xmlns:th-java="urn:opendaylight:params:xml:ns:yang:controller:threadpool:impl">th-java:threadfactory-naming</type>
424 * returns {"th-java","urn:.."}. If no prefix is matched, then default
425 * namespace is returned with empty string as key. If no default namespace
426 * is found value will be null.
428 public Map.Entry<String/* prefix */, String/* namespace */> findNamespaceOfTextContent() throws DocumentedException {
429 Map<String, String> namespaces = extractNamespaces();
430 String textContent = getTextContent();
431 int indexOfColon = textContent.indexOf(':');
433 if (indexOfColon > -1) {
434 prefix = textContent.substring(0, indexOfColon);
436 prefix = DEFAULT_NAMESPACE_PREFIX;
438 if (!namespaces.containsKey(prefix)) {
439 throw new IllegalArgumentException("Cannot find namespace for " + XmlUtil.toString(element) + ". Prefix from content is "
440 + prefix + ". Found namespaces " + namespaces);
442 return Maps.immutableEntry(prefix, namespaces.get(prefix));
445 public List<XmlElement> getChildElementsWithSameNamespace(final String childName) throws MissingNameSpaceException {
446 List<XmlElement> children = getChildElementsWithinNamespace(getNamespace());
447 return Lists.newArrayList(Collections2.filter(children, xmlElement -> xmlElement.getName().equals(childName)));
450 public void checkUnrecognisedElements(final List<XmlElement> recognisedElements,
451 final XmlElement... additionalRecognisedElements) throws DocumentedException {
452 List<XmlElement> childElements = getChildElements();
453 childElements.removeAll(recognisedElements);
454 for (XmlElement additionalRecognisedElement : additionalRecognisedElements) {
455 childElements.remove(additionalRecognisedElement);
457 if (!childElements.isEmpty()){
458 throw new DocumentedException(String.format("Unrecognised elements %s in %s", childElements, this),
459 DocumentedException.ErrorType.APPLICATION,
460 DocumentedException.ErrorTag.INVALID_VALUE,
461 DocumentedException.ErrorSeverity.ERROR);
465 public void checkUnrecognisedElements(final XmlElement... additionalRecognisedElements) throws DocumentedException {
466 checkUnrecognisedElements(Collections.<XmlElement>emptyList(), additionalRecognisedElements);
470 public boolean equals(final Object o) {
474 if (o == null || getClass() != o.getClass()) {
478 XmlElement that = (XmlElement) o;
480 return element.isEqualNode(that.element);
485 public int hashCode() {
486 return element.hashCode();
489 public boolean hasNamespace() {
490 return getNamespaceAttributeOptionally().isPresent() || getNamespaceOptionally().isPresent();
493 private interface ElementFilteringStrategy {
494 boolean accept(Element e);