/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.commons.xml; import org.eclipse.che.commons.xml.XMLTree.Segment; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import static org.eclipse.che.commons.xml.XMLTreeUtil.asElement; import static org.eclipse.che.commons.xml.XMLTreeUtil.asElements; import static java.util.Collections.emptyList; import static java.util.Objects.requireNonNull; import static javax.xml.XMLConstants.XMLNS_ATTRIBUTE; import static javax.xml.XMLConstants.XMLNS_ATTRIBUTE_NS_URI; import static org.w3c.dom.Node.DOCUMENT_NODE; import static org.w3c.dom.Node.ELEMENT_NODE; import static org.w3c.dom.Node.TEXT_NODE; /** * XMLTree element which provides abilities to * fetch and update xml document data. * <p/> * Delegates for related {@link org.w3c.dom.Element} * * @author Eugene Voevodin */ public final class Element { private final XMLTree xmlTree; Segment start; Segment end; List<Segment> text; org.w3c.dom.Element delegate; Element(XMLTree xmlTree) { this.xmlTree = xmlTree; } /** * Returns name of element as <i>prefix:name</i>. * If element doesn't have prefix only local name will be returned * * @return name of element tag as <i>prefix:name</i> * @throws XMLTreeException * when {@link #remove()} has been invoked on this element instance * @see org.w3c.dom.Element#getTagName() */ public String getName() { checkNotRemoved(); return delegate.getTagName(); } /** * Returns local name of element * * @return element local name * @throws XMLTreeException * when this element has been removed from xml tree * @see org.w3c.dom.Element#getLocalName() */ public String getLocalName() { checkNotRemoved(); return delegate.getLocalName(); } /** * Returns element name prefix or {@code null} if element name is not prefixed * * @return element name prefix * @throws XMLTreeException * when this element has been removed from xml tree * @see org.w3c.dom.Element#getPrefix() */ public String getPrefix() { checkNotRemoved(); return delegate.getPrefix(); } /** * Returns element parent or {@code null} if element doesn't have parent. * * @return element parent or {@code null} if element is xml root * @throws XMLTreeException * when this element has been removed from xml tree */ public Element getParent() { checkNotRemoved(); return asElement(delegate.getParentNode()); } /** * Searches for element sibling with given name. * If more than one sibling was found throws {@link XMLTreeException}. * If sibling with given name doesn't exist returns {@code null}. * <p/> * Note that {@link #getName} method used to compare element names. * * @param name * sibling name to search * @return element sibling with given name or {@code null} if sibling with given <i>name</i> was not found * @throws XMLTreeException * when element has more than one sibling with given <i>name</i> * or this element has been removed from xml tree * @throws NullPointerException * when name parameter is {@code null} */ public Element getSingleSibling(String name) { checkNotRemoved(); requireNonNull(name, "Required not null sibling name"); Element target = null; for (Element sibling : asElements(delegate.getParentNode().getChildNodes())) { if (this != sibling && sibling.getName().equals(name)) { if (target != null) { throw new XMLTreeException("Element " + name + " has more than one sibling with name " + name); } target = sibling; } } return target; } /** * Searches for element child with given name. * If element has more then only child with given name then {@link XMLTreeException} will be thrown. * If child with given name doesn't exist returns {@code null} * <p/> * Note that {@link #getName} method used to compare element names. * * @param name * name to search child * @return child element with given name or {@code null} if element with given name was not found * @throws XMLTreeException * when element has more than one child with given <i>name</i> * or this element has been removed from xml tree * @throws NullPointerException * when name parameter is {@code null} */ public Element getSingleChild(String name) { checkNotRemoved(); requireNonNull(name, "Required not null child name"); for (Element child : asElements(delegate.getChildNodes())) { if (name.equals(child.getName())) { if (child.hasSibling(name)) { throw new XMLTreeException("Element " + name + " has more then only child with name " + name + " found"); } return child; } } return null; } /** * Returns last element child or {@code null} if element doesn't have children * * @return last child element or {@code null} if this element doesn't have children * @throws XMLTreeException * when this element has been removed from xml tree */ public Element getLastChild() { checkNotRemoved(); final Node lastChild = delegate.getLastChild(); if (lastChild != null && lastChild.getNodeType() != ELEMENT_NODE) { return asElement(previousElementNode(lastChild)); } return asElement(lastChild); } /** * Returns first element child or {@code null} if element doesn't have children * * @return first child element or {@code null} if this element doesn't have children * @throws XMLTreeException * when this element has been removed from xml tree */ public Element getFirstChild() { checkNotRemoved(); final Node firstChild = delegate.getFirstChild(); if (firstChild.getNodeType() != ELEMENT_NODE) { return asElement(nextElementNode(firstChild)); } return asElement(firstChild); } /** * Returns element children or empty list when element doesn't have children * * @return list of element children * @throws XMLTreeException * when this element has been removed from xml tree */ public List<Element> getChildren() { checkNotRemoved(); return asElements(delegate.getChildNodes()); } /** * Returns children mapped with given mapper or empty list when element doesn't have children * * @param mapper * function which will be applied on each child element * @param <R> * mapper result type * @return list of element children which are mapped with given mapper * @throws XMLTreeException * when this element has been removed from xml tree */ public <R> List<R> getChildren(ElementMapper<? extends R> mapper) { checkNotRemoved(); return asElements(delegate.getChildNodes(), mapper); } /** * Returns element text content. * <p/> * Note that only element text going to be fetched, no CDATA * or children text content. * * @return element text content * @throws XMLTreeException * when this element has been removed from xml tree */ public String getText() { checkNotRemoved(); return fetchText(); } /** * Returns {@code true} if element has at least one sibling with given name, otherwise returns {@code false}. * * @return {@code true} if element has at least one singling with given name, otherwise {@code false}. * @throws XMLTreeException * when this element has been removed from xml tree * @throws NullPointerException * when name parameter is {@code null} */ public boolean hasSibling(String name) { checkNotRemoved(); requireNonNull(name, "Required not null sibling name"); final NodeList nodes = delegate.getParentNode().getChildNodes(); for (int i = 0; i < nodes.getLength(); i++) { if (nodes.item(i) != delegate && name.equals(nodes.item(i).getNodeName())) { return true; } } return false; } /** * Returns {@code true} if this element instance is xml root element, otherwise returns {@code false} * * @return {@code true} if element has parent, otherwise {@code false} * @throws XMLTreeException * when this element has been removed from xml tree */ public boolean hasParent() { checkNotRemoved(); return delegate.getParentNode() != null && delegate.getParentNode().getNodeType() != DOCUMENT_NODE; } /** * Returns previous element sibling or {@code null} when element doesn't have previous sibling * * @return previous element sibling or {@code null} when element doesn't have previous sibling * @throws XMLTreeException * when this element has been removed from xml tree */ public Element getPreviousSibling() { checkNotRemoved(); return asElement(previousElementNode(delegate)); } /** * Returns next element sibling or {@code null} if element doesn't have next sibling * * @return next element sibling or {@code null} if element doesn't have next sibling * @throws XMLTreeException * when this element has been removed from xml tree */ public Element getNextSibling() { checkNotRemoved(); return asElement(nextElementNode(delegate)); } /** * Returns element attributes or empty list if element doesn't have attributes. * <p/> * When element doesn't have attributes returns {@link java.util.Collections#emptyList()} * which is unmodifiable, so clients should not use list 'update' methods. * * @return list of element attributes or empty list if element doesn't have attributes * @throws XMLTreeException * when this element has been removed from xml tree */ public List<Attribute> getAttributes() { checkNotRemoved(); if (delegate != null && delegate.hasAttributes()) { final NamedNodeMap attributes = delegate.getAttributes(); final List<Attribute> copy = new ArrayList<>(attributes.getLength()); for (int i = 0; i < attributes.getLength(); i++) { final Node item = attributes.item(i); copy.add(asAttribute(item)); } return copy; } return emptyList(); } /** * Returns list of element sibling or empty list if element doesn't have siblings. * * @return list of element sibling * @throws XMLTreeException * when this element has been removed from xml tree */ public List<Element> getSiblings() { checkNotRemoved(); final List<Element> siblings = asElements(delegate.getParentNode().getChildNodes()); siblings.remove(asElement(delegate)); return siblings; } /** * Returns {@code true} if element has at least one child with given name, * otherwise returns {@code false}. * * @param name * child name to check * @return {@code true} if element has at least one child with given name, otherwise {@code false} * @throws XMLTreeException * when this element has been removed from xml tree * @throws NullPointerException * when name parameter is {@code null} */ public boolean hasChild(String name) { checkNotRemoved(); requireNonNull(name, "Required not null child name"); final NodeList nodes = delegate.getChildNodes(); for (int i = 0; i < nodes.getLength(); i++) { if (name.equals(nodes.item(i).getNodeName())) { return true; } } return false; } /** * Returns {@code true} if element has at least one child or {@code false} if doesn't * * @return {@code true} if element has at least one child or {@code false} if doesn't * @throws XMLTreeException * when this element has been removed from xml tree */ public boolean hasChildren() { checkNotRemoved(); final NodeList childNodes = delegate.getChildNodes(); for (int i = 0; i < childNodes.getLength(); i++) { if (childNodes.item(i).getNodeType() == ELEMENT_NODE) { return true; } } return false; } /** * Sets new text content to element * * @param newText * new text content * @throws XMLTreeException * when this element has been removed from xml tree * @throws NullPointerException * when newText parameter is {@code null} */ public Element setText(String newText) { checkNotRemoved(); requireNonNull(newText, "Required not null new text"); if (!newText.equals(getText())) { removeTextNodes(); delegate.appendChild(document().createTextNode(newText)); //let tree do dirty job xmlTree.updateText(this); } return this; } /** * Returns text content of child with given name. * * @param childName * child name to fetch text content * @return child text or {@code null} if child doesn't exist or * element has more then only child with given name */ public String getChildText(String childName) { return getChildTextOrDefault(childName, null); } /** * Returns text content of child with given name or * default value if child doesn't exist or it has sibling with same name * * @param childName * name of child * @param defaultValue * value which will be returned if child doesn't exist * or it has sibling with same name * @return child text * @throws XMLTreeException * when this element has been removed from xml tree * @throws NullPointerException * when childName parameter is {@code null} */ public String getChildTextOrDefault(String childName, String defaultValue) { checkNotRemoved(); requireNonNull(childName, "Required not null child name"); return hasSingleChild(childName) ? getSingleChild(childName).getText() : defaultValue; } /** * Returns {@code true} if element has only sibling with given name * or {@code false} if element has more then 1 or 0 siblings with given name * * @param childName * name of sibling * @return {@code true} if element has only sibling with given name otherwise {@code false} * @throws XMLTreeException * when this element has been removed from xml tree * @throws NullPointerException * when childName parameter is {@code null} */ public boolean hasSingleChild(String childName) { checkNotRemoved(); requireNonNull(childName, "Required not null child name"); for (Element child : asElements(delegate.getChildNodes())) { if (childName.equals(child.getName())) { return !child.hasSibling(childName); } } return false; } /** * Removes single element child. * If child does not exist nothing will be done * * @param name * child name to removeElement * @return this element instance * @throws XMLTreeException * when this element has been removed from xml tree */ public Element removeChild(String name) { checkNotRemoved(); final Element child = getSingleChild(name); if (child != null) { child.remove(); } return this; } /** * Removes current element and related children from xml * * @throws XMLTreeException * when this element has been removed from xml tree * or this element is root element */ public void remove() { checkNotRemoved(); notPermittedOnRootElement(); if (hasChildren()) { for (Element element : getChildren()) { element.remove(); } } //let tree do dirty job xmlTree.removeElement(this); //remove self from document delegate.getParentNode().removeChild(delegate); //if references to 'this' element exist //we should disallow ability to use delegate delegate = null; } /** * Removes children which names equal to given name * * @param name * name to remove children * @return this element instance * @throws XMLTreeException * when this element has been removed from xml tree */ public Element removeChildren(String name) { checkNotRemoved(); final List<Node> matched = new LinkedList<>(); final NodeList nodes = delegate.getChildNodes(); for (int i = 0; i < nodes.getLength(); i++) { if (name.equals(nodes.item(i).getNodeName())) { matched.add(nodes.item(i)); } } for (Node node : matched) { asElement(node).remove(); } return this; } /** * Sets new attribute to element. * If element has attribute with given name * attribute value will be replaced with new value * * @param name * attribute name * @param value * attribute value * @return this element instance * @throws XMLTreeException * when this element has been removed from xml tree */ public Element setAttribute(String name, String value) { return setAttribute(new NewAttribute(name, value)); } /** * Sets new attribute to element. * If element has attribute with {@code newAttribute#name} * then existing attribute value will be replaced with {@code newAttribute#value}. * * @param newAttribute * attribute that should be added to element * @return this element instance * @throws XMLTreeException * when this element has been removed from xml tree */ public Element setAttribute(NewAttribute newAttribute) { checkNotRemoved(); requireNonNull(newAttribute, "Required not null new attribute"); //if tree already contains element replace value if (hasAttribute(newAttribute.getName())) { final Attribute attr = getAttribute(newAttribute.getName()); attr.setValue(newAttribute.getValue()); return this; } // if (newAttribute.hasPrefix()) { delegate.setAttributeNodeNS(createAttrNSNode(newAttribute)); } else { delegate.setAttributeNode(createAttrNode(newAttribute)); } //let tree do dirty job xmlTree.insertAttribute(newAttribute, this); return this; } /** * Removes attribute with given name. * If element doesn't have attribute with given name * nothing will be done. * * @param name * name of attribute which should be removed from element * @return this element instance * @throws XMLTreeException * when this element has been removed from xml tree * @throws NullPointerException * when name parameter is {@code null} */ public Element removeAttribute(String name) { checkNotRemoved(); final Attribute attribute = getAttribute(name); if (attribute != null) { xmlTree.removeAttribute(attribute); delegate.getAttributes() .removeNamedItem(name); } return this; } /** * Returns {@code true} if element has attribute with given name * * @param name * name of attribute to check * @return {@code true} if element has attribute with {@code name} otherwise {@code false} * @throws XMLTreeException * when this element has been removed from xml tree */ public boolean hasAttribute(String name) { checkNotRemoved(); return delegate.hasAttribute(name); } /** * Returns {@code true} if element doesn't have closing tag * i.e {@literal <element attr="value"/>}, otherwise {@code false} */ public boolean isVoid() { return start.equals(end); } /** * Returns attribute with given name or {@code null} * if element doesn't have such attribute * * @param name * name to search attribute * @return attribute with {@code name} or {@code null} if nothing found * @throws XMLTreeException * when this element has been removed from xml tree * @throws NullPointerException * when name parameter is {@code null} */ public Attribute getAttribute(String name) { checkNotRemoved(); requireNonNull(name, "Required not null attribute name"); if (delegate.hasAttributes()) { return asAttribute(getAttributeNode(name)); } return null; } /** * Replaces this element with new one. * * @param newElement * new element which is replacement for current element * @return newly created element * @throws XMLTreeException * when this element has been removed from xml tree * or this element is root element * @throws NullPointerException * when newElement parameter is {@code null} */ public Element replaceWith(NewElement newElement) { checkNotRemoved(); notPermittedOnRootElement(); requireNonNull(newElement, "Required not null new element"); insertAfter(newElement); final Element inserted = getNextSibling(); remove(); return inserted; } /** * Appends new element to the end of children list * * @param newElement * element which will be inserted to the end of children list * @return this element instance * @throws XMLTreeException * when this element has been removed from xml tree * @throws NullPointerException * when newElement parameter is {@code null} */ public Element appendChild(NewElement newElement) { checkNotRemoved(); requireNonNull(newElement, "Required not null new element"); if (isVoid()) { throw new XMLTreeException("Append child is not permitted on void elements"); } final Node newNode = createNode(newElement); final Element element = createElement(newNode); //append new node into document delegate.appendChild(newNode); //let tree do dirty job xmlTree.appendChild(newElement, element, this); return this; } /** * Inserts new element after current * * @param newElement * element which will be inserted after current * @return this element instance * @throws XMLTreeException * when this element has been removed from xml tree * or this element is root element * @throws NullPointerException * when newElement parameter is {@code null} */ public Element insertAfter(NewElement newElement) { checkNotRemoved(); notPermittedOnRootElement(); requireNonNull(newElement, "Required not null new element"); final Node newNode = createNode(newElement); final Element element = createElement(newNode); //if element has next sibling append child to parent //else insert before next sibling final Node nextNode = nextElementNode(delegate); if (nextNode != null) { delegate.getParentNode().insertBefore(newNode, nextNode); } else { delegate.getParentNode().appendChild(newNode); } //let tree do dirty job xmlTree.insertAfter(newElement, element, this); return this; } /** * Inserts new element before current element * * @param newElement * element which will be inserted before current * @return this element instance * @throws XMLTreeException * when this element has been removed from xml tree * or this element is root element * @throws NullPointerException * when newElement parameter is {@code null} */ public Element insertBefore(NewElement newElement) { checkNotRemoved(); notPermittedOnRootElement(); requireNonNull(newElement, "Required not null new element"); //if element has previous sibling insert new element after it //inserting before this element to let existing comments //or whatever over referenced element if (previousElementNode(delegate) != null) { getPreviousSibling().insertAfter(newElement); return this; } final Node newNode = createNode(newElement); final Element element = createElement(newNode); delegate.getParentNode().insertBefore(newNode, delegate); //let tree do dirty job xmlTree.insertAfterParent(newElement, element, getParent()); return this; } /** * Adds new element as child to the specified by {@link XMLTreeLocation} location. * <p/> * If it is not possible to insert element in specified location * then {@link XMLTreeException} will be thrown * * @param child * new child */ public Element insertChild(NewElement child, XMLTreeLocation place) { place.evalInsert(this, child); return this; } void setAttributeValue(Attribute attribute) { checkNotRemoved(); final Node attributeNode = getAttributeNode(attribute.getName()); xmlTree.updateAttributeValue(attribute, attributeNode.getNodeValue()); getAttributeNode(attribute.getName()).setNodeValue(attribute.getValue()); } private void removeTextNodes() { final NodeList childNodes = delegate.getChildNodes(); for (int i = 0; i < childNodes.getLength(); i++) { if (childNodes.item(i).getNodeType() == TEXT_NODE) { delegate.removeChild(childNodes.item(i)); } } } private Attribute asAttribute(Node node) { if (node == null) { return null; } return new Attribute(this, node.getNodeName(), node.getNodeValue()); } private String fetchText() { final StringBuilder sb = new StringBuilder(); final NodeList childNodes = delegate.getChildNodes(); for (int i = 0; i < childNodes.getLength(); i++) { if (childNodes.item(i).getNodeType() == TEXT_NODE) { sb.append(childNodes.item(i).getTextContent()); } } return sb.toString(); } private Attr createAttrNode(NewAttribute newAttribute) { final Attr attr = document().createAttribute(newAttribute.getName()); attr.setValue(newAttribute.getValue()); return attr; } private Attr createAttrNSNode(NewAttribute attribute) { if (attribute.getPrefix().equals(XMLNS_ATTRIBUTE)) { final Attr attr = document().createAttributeNS(XMLNS_ATTRIBUTE_NS_URI, attribute.getName()); attr.setValue(attribute.getValue()); //save uri xmlTree.putNamespace(attribute.getLocalName(), attribute.getValue()); return attr; } else { //retrieve namespace final String uri = xmlTree.getNamespaceUri(attribute.getPrefix()); final Attr attr = document().createAttributeNS(uri, attribute.getName()); attr.setValue(attribute.getValue()); return attr; } } private Node nextElementNode(Node node) { node = node.getNextSibling(); while (node != null && node.getNodeType() != ELEMENT_NODE) { node = node.getNextSibling(); } return node; } private Node previousElementNode(Node node) { node = node.getPreviousSibling(); while (node != null && node.getNodeType() != ELEMENT_NODE) { node = node.getPreviousSibling(); } return node; } private void notPermittedOnRootElement() { if (!hasParent()) { throw new XMLTreeException("Operation not permitted for root element"); } } private void checkNotRemoved() { if (delegate == null) { throw new XMLTreeException("Operation not permitted for element which has been removed from XMLTree"); } } private Element createElement(Node node) { final Element element = new Element(xmlTree); element.delegate = (org.w3c.dom.Element)node; node.setUserData("element", element, null); if (node.hasChildNodes()) { final NodeList children = node.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { if (children.item(i).getNodeType() == ELEMENT_NODE) { createElement(children.item(i)); } } } return element; } private Node createNode(NewElement newElement) { final org.w3c.dom.Element newNode; if (newElement.hasPrefix()) { final String uri = xmlTree.getNamespaceUri(newElement.getPrefix()); newNode = document().createElementNS(uri, newElement.getName()); } else { newNode = document().createElement(newElement.getLocalName()); } newNode.setTextContent(newElement.getText()); //creating all related children for (NewElement child : newElement.getChildren()) { newNode.appendChild(createNode(child)); } //creating all related attributes for (NewAttribute attribute : newElement.getAttributes()) { if (attribute.hasPrefix()) { newNode.setAttributeNodeNS(createAttrNSNode(attribute)); } else { newNode.setAttributeNode(createAttrNode(attribute)); } } return newNode; } private Document document() { return delegate.getOwnerDocument(); } private Node getAttributeNode(String name) { final NamedNodeMap attributes = delegate.getAttributes(); for (int i = 0; i < attributes.getLength(); i++) { if (attributes.item(i).getNodeName().equals(name)) { return attributes.item(i); } } return null; } }