/*******************************************************************************
* Copyright (c) 2008, 2009 Bug Labs, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* - Neither the name of Bug Labs, Inc. nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/
package com.buglabs.util.xml;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;
/**
* This class represents an XML Node. Any DOM document is a tree of
* <code>XMLNode</code>s.
*
* This implementation of XmlNode uses the xpp library for parsing and serialization.
*
* @author kgilmer
*
*/
public class XmlNode {
private static XmlSerializer serializer;
private static XmlPullParserFactory factory;
private static XmlPullParser parser;
/**
* Name of node.
*/
private String name = null;
/**
* Content of node.
*/
private String value = null;
private Map<String, String> attributes;
private List<XmlNode> children;
private XmlNode parentNode = null;
/**
* Create an empty node.
*
* @param tagName Name of tag. RuntimeExeception will be thrown if null value is passed.
*/
public XmlNode(String tagName) {
if (tagName == null)
throw new RuntimeException("Tag name cannot be null");
this.name = tagName;
attributes = new HashMap<String, String>();
}
/**
* Create a node with a String value.
*
* @param tagName Name of tag. RuntimeExeception will be thrown if null value is passed.
* @param value Contents of node. Null is safe to pass.
*/
public XmlNode(String tagName, String value) {
this(tagName);
if (value != null && value.length() > 0) {
this.value = value;
}
}
/**
* Create a node with children.
*
* @param tagName Name of tag. RuntimeExeception will be thrown if null value is passed.
* @param children List<XmlNode> of child nodes.
*/
public XmlNode(String tagName, List<XmlNode> children) {
this(tagName);
this.children = children;
}
/**
* Create a node with a parent.
*
* @param parent Parent XmlNode.
* @param tagName Name of tag. RuntimeExeception will be thrown if null value is passed.
*/
public XmlNode(XmlNode parent, String tagName) {
this(tagName);
parent.addChild(this);
this.parentNode = parent;
}
/**
* Create a node with a parent and children.
*
* @param parent Parent XmlNode
* @param tagName Name of tag. RuntimeExeception will be thrown if null value is passed.
* @param children List<XmlNode> of child nodes.
*/
public XmlNode(XmlNode parent, String tagName, List<XmlNode> children) {
this(tagName);
parent.addChild(this);
this.parentNode = parent;
this.children = children;
}
/**
* Create a node with a parent and a String value.
*
* @param parent Parent XmlNode.
* @param tagName Name of tag. RuntimeExeception will be thrown if null value is passed.
* @param value List<XmlNode> of child nodes.
*/
public XmlNode(XmlNode parent, String tagName, String value) {
this(parent, tagName);
if (value != null && value.length() > 0) {
this.value = value;
}
}
/**
* @return true if the node has children, false otherwise.
*/
public boolean hasChildren() {
return children != null && children.size() > 0;
}
/**
* @return Name of this node.
*/
public String getName() {
return name;
}
/**
* @param name name of attribute
* @param value value of attribute
* @return instance of self
*/
public XmlNode addAttribute(String name, String value) {
this.getAttributes().put(name, value);
return this;
}
/**
* Set the name of the tag.
*
* @param tagName Name of tag.
* @return instance of node.
*/
public XmlNode setName(String tagName) {
this.name = tagName;
return this;
}
/**
* @return value of tag. Can be null.
*/
public String getValue() {
return value;
}
/**
* @return true if value is not null
*/
public boolean hasValue() {
return value != null;
}
/**
* @param value Value of node. Can only be called on nodes that do not contain children.
* Runtime exception will be generated otherwise.
* @return instance of node.
*/
public XmlNode setValue(String value) {
if (value == null) {
clearValue();
} else {
if (hasValue())
throw new RuntimeException("Cannot set content on a node that has children.");
}
this.value = value;
return this;
}
/**
* Get contents of attribute, or null if attribute does not exist.
*
* @param name name of attribute
* @return value of attribute or null if does not exist.
*/
public String getAttribute(String name) {
return (String) attributes.get(name);
}
/**
* @param name attribute name
* @param value attribute value
* @return instance of XmlNode
*/
public XmlNode setAttribute(String name, String value) {
attributes.put(name, value);
return this;
}
/**
* Clear the value of the XML node.
* @return instance of XmlNode
*/
public XmlNode clearValue() {
value = null;
return this;
}
/**
* Add a child XmlNode to the current node. Runtime exception will be thrown if a node is added to itself.
* Runtime exception will also be generated if parent node already contains a value.
*
* @param element Child XmlNode
* @return instance of XmlNode
*/
public XmlNode addChild(XmlNode element) {
if (element == this) {
throw new RuntimeException("Cannot add node to itself.");
}
if (this.value != null) {
throw new RuntimeException("Cannot add child elements to a node that has content.");
}
getChildren().add(element);
element.setParent(this);
return element;
}
/**
* @return children of node, or empty list if no children exist.
*/
public List<XmlNode> getChildren() {
if (children == null) {
children = new ArrayList<XmlNode>();
}
return children;
}
/**
* @return Map of attribute names and values as Strings.
*/
public Map<String, String> getAttributes() {
return attributes;
}
/**
* @return depth of this node in the DOM
*/
public int getDepth() {
if (parentNode == null)
return 0;
int count = 0;
XmlNode p = this;
while ((p = p.getParent()) != null) {
count++;
}
return count;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
public String toString() {
try {
return serialize(this);
} catch (Exception e) {
//Intentionally ignore any exception and fall back to toString() on Object.
}
return super.toString();
}
/**
* @param name name of child node
* @return true if a node with the given name exists, false otherwise.
*/
public boolean hasChild(String name) {
if (!hasChildren()) {
return false;
}
if (getChild(name) != null) {
return true;
}
return false;
}
/**
* @param name name of child node
* @deprecated use hasChild()
* @return true if a node with the given name exists, false otherwise.
*/
public boolean childExists(String name) {
return hasChild(name);
}
/**
* @param nodeName name of child node
* @return node with given name if exists or null otherwise.
*/
public XmlNode getChild(String nodeName) {
if (children == null) {
return null;
}
for (Iterator<XmlNode> i = getChildren().iterator(); i.hasNext();) {
XmlNode child = i.next();
if (child.name.equals(nodeName)) {
return child;
}
}
return null;
}
/**
* Retrieve a node from this element using xpath-like notation.
*
* Example: for "<root><leaf1></leaf1><leaf2/></root>" call with "root/leaf1" to
* return first occurrence leaf1 node.
*
* @param path tree path expressed as node names delimited with '/' character.
* @return The node or null if not found.
*/
public XmlNode getFirstElement(String path) {
String[] elems = path.split("/");
XmlNode root = this;
for (int i = 0; i < elems.length; ++i) {
root = (XmlNode) root.getChild(elems[i]);
if (root == null) {
break;
}
}
return root;
}
/**
* @return The parent node or <code>null</code> if root node in DOM.
*/
public XmlNode getParent() {
return parentNode;
}
/**
* Set the parent node.
* @param parent parent node
* @return instance of XmlNode
*/
public XmlNode setParent(XmlNode parent) {
if (parentNode != null) {
parentNode.getChildren().remove(this);
}
parentNode = parent;
return this;
}
/**
* Serialize an XmlNode into a String.
*
* @param node XmlNode
* @return String of XML
* @throws XmlPullParserException on parse errors
* @throws IOException on I/O errors
*/
public static String serialize(XmlNode node) throws XmlPullParserException, IOException {
if (serializer == null) {
if (factory == null) {
factory = XmlPullParserFactory.newInstance(
"org.xmlpull.mxp1.MXParser,org.xmlpull.mxp1_serializer.MXSerializer", null);
}
serializer = factory.newSerializer();
serializer.setProperty("http://xmlpull.org/v1/doc/properties.html#serializer-indentation", " ");
serializer.setFeature("http://xmlpull.org/v1/doc/features.html#serializer-attvalue-use-apostrophe", true);
}
Writer writer = new StringWriter();
serializer.setOutput(writer);
walkTree(node, serializer);
return writer.toString();
}
/**
* @param node XmlNode
* @param serializer XmlSerializer
* @throws IOException on IO error
*/
private static void walkTree(XmlNode node, XmlSerializer serializer)
throws IOException {
serializer.startTag(null, node.getName());
for (String attrName : node.getAttributes().keySet()) {
serializer.attribute(null, attrName, node.getAttribute(attrName));
}
if (node.hasValue()) {
serializer.text(node.getValue());
} else if (node.hasChildren()) {
for (XmlNode c : node.children)
walkTree(c, serializer);
}
serializer.endTag(null, node.getName());
}
/**
* Parse an xml string into an XmlNode.
*
* @param xmlString String of XML
* @return XmlNode
* @throws IOException on I/O errors
*/
public static XmlNode parse(String xmlString) throws IOException {
try {
parser = getParser(true);
parser.setInput(new StringReader(xmlString));
return parse(parser, null);
} catch (XmlPullParserException e) {
throw new IOException(e);
}
}
/**
* @param xmlString input XML as a string
* @param isNamespaceAware if namespaces should be parsed
* @return An XmlNode that corresponds to the root of the parsed xmlString.
* @throws IOException on I/O or XML parse error.
*/
public static XmlNode parse(String xmlString, boolean isNamespaceAware) throws IOException {
try {
parser = getParser(isNamespaceAware);
parser.setInput(new StringReader(xmlString));
return parse(parser, null);
} catch (XmlPullParserException e) {
throw new IOException(e);
}
}
/**
* Parse an xml string into an XmlNode.
*
* @param xmlReader Reader of XML document
* @return XmlNode
* @throws IOException on I/O errors
*/
public static XmlNode parse(Reader xmlReader) throws IOException {
try {
parser = getParser(true);
parser.setInput(xmlReader);
return parse(parser, null);
} catch (XmlPullParserException e) {
throw new IOException(e);
}
}
/**
* @param xmlReader input XML
* @param isNamespaceAware if namespaces should be parsed
* @return An XmlNode that corresponds to the root of the parsed xmlString.
* @throws IOException on I/O or XML parse error.
*/
public static XmlNode parse(Reader xmlReader, boolean isNamespaceAware) throws IOException {
try {
parser = getParser(isNamespaceAware);
parser.setInput(xmlReader);
return parse(parser, null);
} catch (XmlPullParserException e) {
throw new IOException(e);
}
}
/**
* @param isNamespaceAware if namespaces should be parsed
* @return An XmlNode that corresponds to the root of the parsed xmlString.
* @throws XmlPullParserException on parse error
*/
private static XmlPullParser getParser(boolean isNamespaceAware) throws XmlPullParserException {
if (parser == null) {
if (factory == null) {
factory = XmlPullParserFactory.newInstance(
"org.xmlpull.mxp1.MXParser,org.xmlpull.mxp1_serializer.MXSerializer", null);
}
factory.setNamespaceAware(isNamespaceAware);
parser = factory.newPullParser();
}
parser = factory.newPullParser();
return parser;
}
/**
* @param pullParser parser
* @param node XmlNode
* @return An XmlNode that corresponds to the root of the parsed xmlString.
* @throws XmlPullParserException on xml parse error
* @throws IOException on I/O error
*/
private static XmlNode parse(XmlPullParser pullParser, XmlNode node) throws XmlPullParserException, IOException {
while (true) {
switch (parser.next()) {
case XmlPullParser.START_TAG:
if (node == null)
node = new XmlNode(parser.getName());
else
node = new XmlNode(node, parser.getName());
for (int i = 0; i < parser.getAttributeCount(); ++i) {
node.addAttribute(parser.getAttributeName(i), parser.getAttributeValue(i));
}
break;
case XmlPullParser.TEXT:
if (!parser.isWhitespace())
node.setValue(parser.getText());
break;
case XmlPullParser.END_TAG:
if (node.getParent() != null)
node = node.getParent();
break;
case XmlPullParser.END_DOCUMENT:
return node;
default:
break;
}
}
}
}