/*
* utils - SimpleXML.java - Copyright © 2006-2009 David Roden
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.pterodactylus.util.xml;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import net.pterodactylus.util.logging.Logging;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
/**
* SimpleXML is a helper class to construct XML trees in a fast and simple way.
* Construct a new XML tree by calling {@link #SimpleXML(String)} and append new
* nodes by calling {@link #append(String)}.
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
public class SimpleXML {
/** Logger. */
private static final Logger logger = Logging.getLogger(SimpleXML.class.getName());
/**
* A {@link List} containing all child nodes of this node.
*/
private List<SimpleXML> children = new ArrayList<SimpleXML>();
/**
* The name of this node.
*/
private String name = null;
/**
* The value of this node.
*/
private String value = null;
/** Attributes of the element. */
private Map<String, String> attributes = null;
/**
* Constructs a new XML node without a name.
*/
public SimpleXML() {
super();
}
/**
* Constructs a new XML node with the specified name.
*
* @param name
* The name of the new node
*/
public SimpleXML(String name) {
this(name, (String[]) null, (String[]) null);
}
/**
* Constructs a new XML node with the specified name and a single attribute.
*
* @param name
* The name of the node
* @param attributeName
* The name of the attribute
* @param attributeValue
* The value of the attribute
*/
public SimpleXML(String name, String attributeName, String attributeValue) {
this(name, new String[] { attributeName }, new String[] { attributeValue });
}
/**
* Constructs a new XML node with the specified name and attributes.
*
* @param name
* The name of the node
* @param attributeNames
* The names of the attribute
* @param attributeValues
* The values of the attribute
*/
public SimpleXML(String name, String[] attributeNames, String[] attributeValues) {
this.name = name;
attributes = new HashMap<String, String>();
if ((attributeNames != null) && (attributeValues != null) && (attributeNames.length == attributeValues.length)) {
for (int index = 0, size = attributeNames.length; index < size; index++) {
attributes.put(attributeNames[index], attributeValues[index]);
}
}
}
/**
* Returns all attributes’ names. The array is not sorted.
*
* @return The names of all attributes
*/
public String[] getAttributeNames() {
return attributes.keySet().toArray(new String[attributes.size()]);
}
/**
* Returns the value of the attribute with the given name.
*
* @param attributeName
* The name of the attribute to look up
* @return The value of the attribute
*/
public String getAttribute(String attributeName) {
return getAttribute(attributeName, null);
}
/**
* Returns the value of the attribute with the given name.
*
* @param attributeName
* The name of the attribute to look up
* @param defaultValue
* The value to return if there is no attribute with the given
* name
* @return The value of the attribute
*/
public String getAttribute(String attributeName, String defaultValue) {
if (!attributes.containsKey(attributeName)) {
return defaultValue;
}
return attributes.get(attributeName);
}
/**
* Sets the value of an attribute.
*
* @param attributeName
* The name of the attribute to set
* @param attributeValue
* The value of the attribute
*/
public void setAttribute(String attributeName, String attributeValue) {
attributes.put(attributeName, attributeValue);
}
/**
* Removes the attribute with the given name, returning its previous value.
*
* @param attributeName
* The name of the attribute to remove
* @return The value of the attribute before removing it
*/
public String removeAttribute(String attributeName) {
return attributes.remove(attributeName);
}
/**
* Checks whether this node contains the attribute with the given name.
*
* @param attributeName
* The name of the attribute
* @return <code>true</code> if this node has an attribute with the given
* name, <code>false</code> otherwise
*/
public boolean hasAttribute(String attributeName) {
return attributes.containsKey(attributeName);
}
/**
* Returns whether this node has any child nodes.
*
* @return {@code true} if this node has child nodes, {@code false}
* otherwise
*/
public boolean hasNodes() {
return !children.isEmpty();
}
/**
* Checks if this object has a child with the specified name.
*
* @param nodeName
* The name of the child node to check for
* @return <code>true</code> if this node has at least one child with the
* specified name, <code>false</code> otherwise
*/
public boolean hasNode(String nodeName) {
return getNode(nodeName) != null;
}
/**
* Returns the child node of this node with the specified name. If there are
* several child nodes with the specified name only the first node is
* returned.
*
* @param nodeName
* The name of the child node
* @return The child node, or <code>null</code> if there is no child node
* with the specified name
*/
public SimpleXML getNode(String nodeName) {
for (int index = 0, count = children.size(); index < count; index++) {
if (children.get(index).name.equals(nodeName)) {
return children.get(index);
}
}
return null;
}
/**
* Returns the child node that is specified by the names. The first element
* of <code>nodeNames</code> is the name of the child node of this node, the
* second element of <code>nodeNames</code> is the name of a child node's
* child node, and so on. By using this method you can descend into an XML
* tree pretty fast.
*
* <pre>
* <code>
* SimpleXML deepNode = topNode.getNodes(new String[] { "person", "address", "number" });
* </code>
* </pre>
*
* @param nodeNames
* The names of the nodes
* @return A node that is a deep child of this node, or <code>null</code> if
* the specified node does not eixst
*/
public SimpleXML getNode(String[] nodeNames) {
SimpleXML node = this;
for (String nodeName : nodeNames) {
node = node.getNode(nodeName);
}
return node;
}
/**
* Returns all child nodes of this node.
*
* @return All child nodes of this node
*/
public SimpleXML[] getNodes() {
return getNodes(null);
}
/**
* Returns all child nodes of this node with the specified name. If there
* are no child nodes with the specified name an empty array is returned.
*
* @param nodeName
* The name of the nodes to retrieve, or <code>null</code> to
* retrieve all nodes
* @return All child nodes with the specified name
*/
public SimpleXML[] getNodes(String nodeName) {
List<SimpleXML> resultList = new ArrayList<SimpleXML>();
for (SimpleXML child : children) {
if ((nodeName == null) || child.name.equals(nodeName)) {
resultList.add(child);
}
}
return resultList.toArray(new SimpleXML[resultList.size()]);
}
/**
* Appends a new XML node with the specified name and returns the new node.
* With this method you can create deep structures very fast.
*
* <pre>
* <code>
* SimpleXML mouseNode = topNode.append("computer").append("bus").append("usb").append("mouse");
* </code>
* </pre>
*
* @param nodeName
* The name of the node to append as a child to this node
* @return The new node
*/
public SimpleXML append(String nodeName) {
return append(new SimpleXML(nodeName));
}
/**
* Appends a new XML node with the specified name and value and returns the
* new node.
*
* @param nodeName
* The name of the node to append
* @param nodeValue
* The value of the node to append
* @return The newly appended node
*/
public SimpleXML append(String nodeName, String nodeValue) {
return append(nodeName).setValue(nodeValue);
}
/**
* Appends the node with all its child nodes to this node and returns the
* child node.
*
* @param newChild
* The node to append as a child
* @return The child node that was appended
*/
public SimpleXML append(SimpleXML newChild) {
children.add(newChild);
return newChild;
}
/**
* Removes the specified child from this node.
*
* @param child
* The child to remove
*/
public void remove(SimpleXML child) {
children.remove(child);
}
/**
* Removes the child with the specified name from this node. If more than
* one children have the same name only the first is removed.
*
* @param childName
* The name of the child node to remove
*/
public void remove(String childName) {
SimpleXML child = getNode(childName);
if (child != null) {
remove(child);
}
}
/**
* Replace the child node with the specified name by a new node with the
* specified content.
*
* @param childName
* The name of the child to replace
* @param value
* The node child's value
*/
public void replace(String childName, String value) {
remove(childName);
append(childName, value);
}
/**
* Replaces the child node that has the same name as the given node by the
* given node.
*
* @param childNode
* The node to replace the previous child node with the same name
*/
public void replace(SimpleXML childNode) {
remove(childNode.getName());
append(childNode);
}
/**
* Removes all children from this node.
*/
public void removeAll() {
children.clear();
}
/**
* Sets the value of this node.
*
* @param nodeValue
* The new value of this node
* @return This node
*/
public SimpleXML setValue(String nodeValue) {
value = nodeValue;
return this;
}
/**
* Returns the name of this node.
*
* @return The name of this node
*/
public String getName() {
return name;
}
/**
* Returns the value of this node.
*
* @return The value of this node
*/
public String getValue() {
return value;
}
/**
* Returns the value of the first child node with the specified name.
*
* @param childName
* The name of the child node
* @return The value of the child node
* @throws NullPointerException
* if the child node does not exist
*/
public String getValue(String childName) {
return getNode(childName).getValue();
}
/**
* Returns the value of the first child node with the specified name, or the
* default value if there is no child node with the given name.
*
* @param childName
* The name of the child node
* @param defaultValue
* The default value to return if there is no child node with the
* given name
* @return The value of the child node
* @throws NullPointerException
* if the child node does not exist
*/
public String getValue(String childName, String defaultValue) {
SimpleXML childNode = getNode(childName);
if (childNode == null) {
return defaultValue;
}
return childNode.getValue();
}
/**
* Creates a {@link Document} from this node and all its child nodes.
*
* @return The {@link Document} created from this node
*/
public Document getDocument() {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document document = documentBuilder.newDocument();
Element rootElement = document.createElement(name);
for (Entry<String, String> attributeEntry : attributes.entrySet()) {
rootElement.setAttribute(attributeEntry.getKey(), attributeEntry.getValue());
}
document.appendChild(rootElement);
addChildren(rootElement);
return document;
} catch (ParserConfigurationException e) {
/* ignore. */
}
return null;
}
/**
* Appends all children of this node to the specified {@link Element}. If a
* node has a value that is not <code>null</code> the value is appended as a
* text node.
*
* @param rootElement
* The element to attach this node's children to
*/
private void addChildren(Element rootElement) {
for (SimpleXML child : children) {
Element childElement = rootElement.getOwnerDocument().createElement(child.name);
for (Entry<String, String> attributeEntry : child.attributes.entrySet()) {
childElement.setAttribute(attributeEntry.getKey(), attributeEntry.getValue());
}
rootElement.appendChild(childElement);
if (child.value != null) {
Text childText = rootElement.getOwnerDocument().createTextNode(child.value);
childElement.appendChild(childText);
} else {
child.addChildren(childElement);
}
}
}
/**
* Creates a SimpleXML node from the specified {@link Document}. The
* SimpleXML node of the document's top-level node is returned.
*
* @param document
* The {@link Document} to create a SimpleXML node from
* @return The SimpleXML node created from the document's top-level node
*/
public static SimpleXML fromDocument(Document document) {
SimpleXML xmlDocument = new SimpleXML(document.getFirstChild().getNodeName());
NamedNodeMap attributes = document.getFirstChild().getAttributes();
for (int attributeIndex = 0, attributeCount = attributes.getLength(); attributeIndex < attributeCount; attributeIndex++) {
Node attribute = attributes.item(attributeIndex);
logger.log(Level.FINER, "adding attribute: " + attribute.getNodeName() + " = " + attribute.getNodeValue());
xmlDocument.setAttribute(attribute.getNodeName(), attribute.getNodeValue());
}
document.normalizeDocument();
/* look for first non-comment node */
Node firstChild = null;
NodeList children = document.getChildNodes();
for (int index = 0, count = children.getLength(); index < count; index++) {
Node child = children.item(index);
if ((child.getNodeType() != Node.COMMENT_NODE) && (child.getNodeType() != Node.PROCESSING_INSTRUCTION_NODE)) {
firstChild = child;
break;
}
}
return addDocumentChildren(xmlDocument, firstChild);
}
/**
* Appends the child nodes of the specified {@link Document} to this node.
* Text nodes are converted into a node's value.
*
* @param xmlDocument
* The SimpleXML node to append the child nodes to
* @param document
* The document whose child nodes to append
* @return The SimpleXML node the child nodes were appended to
*/
private static SimpleXML addDocumentChildren(SimpleXML xmlDocument, Node document) {
NodeList childNodes = document.getChildNodes();
for (int childIndex = 0, childCount = childNodes.getLength(); childIndex < childCount; childIndex++) {
Node childNode = childNodes.item(childIndex);
if ((childNode.getChildNodes().getLength() == 1) && (childNode.getFirstChild().getNodeName().equals("#text"))) {
SimpleXML newXML = xmlDocument.append(childNode.getNodeName(), childNode.getFirstChild().getNodeValue());
NamedNodeMap childNodeAttributes = childNode.getAttributes();
for (int attributeIndex = 0, attributeCount = childNodeAttributes.getLength(); attributeIndex < attributeCount; attributeIndex++) {
Node attribute = childNodeAttributes.item(attributeIndex);
logger.log(Level.FINER, "adding attribute: " + attribute.getNodeName() + " = " + attribute.getNodeValue());
newXML.setAttribute(attribute.getNodeName(), attribute.getNodeValue());
}
} else {
if ((childNode.getNodeType() == Node.ELEMENT_NODE) || (childNode.getChildNodes().getLength() != 0)) {
SimpleXML newXML = xmlDocument.append(childNode.getNodeName());
NamedNodeMap childNodeAttributes = childNode.getAttributes();
for (int attributeIndex = 0, attributeCount = childNodeAttributes.getLength(); attributeIndex < attributeCount; attributeIndex++) {
Node attribute = childNodeAttributes.item(attributeIndex);
logger.log(Level.FINER, "adding attribute: " + attribute.getNodeName() + " = " + attribute.getNodeValue());
newXML.setAttribute(attribute.getNodeName(), attribute.getNodeValue());
}
addDocumentChildren(newXML, childNode);
}
}
}
return xmlDocument;
}
}