/*******************************************************************************
* 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;
}
}