package folioxml.xml;
import folioxml.core.InvalidMarkupException;
import folioxml.core.TokenBase;
import folioxml.slx.SlxToken;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class Node extends TokenBase<Node> {
/*
* Should parse a well-formed set of tokens. Any tags than open must be closed, and any tags that close must be opened.
*
* Can use XmlTokenReader
*/
public Node() {
}
public Node(SlxToken t, boolean deepCopyAttrs) {
if (t != null) {
t.copyTo(this, deepCopyAttrs);
if (this.isClosing()) this.deleteAttributes();
}
}
public Node(String xml) throws IOException, InvalidMarkupException {
XmlTokenReader xtr = new XmlTokenReader(new StringReader(xml)); //Get the token reader
XmlToken t = xtr.read();
t.copyTo(this, false);
if (this.isTag() && this.isOpening()) {
children = new NodeList();
children.setParent(this);
children.addUntil(xtr, this.getTagName());
}
//There should be no remaining tokens.
while (xtr.canRead()) {
XmlToken temp = xtr.read();
if (temp != null && temp.toString().length() > 0)
throw new InvalidMarkupException("Unexpected token: \"" + temp.toString() + "\".", temp);
}
}
public Node(XmlToken t, IXmlTokenReader reader, boolean deepCopyAttrs) throws IOException, InvalidMarkupException {
t.copyTo(this, deepCopyAttrs);
if (this.isTag() && this.isOpening()) {
children = new NodeList();
children.setParent(this);
children.addUntil(reader, this.getTagName());
}
}
public Node parent = null;
public NodeList children = null;
private boolean _deleted = false;
public void markDeleted() {
_deleted = true;
}
public Node remove() {
return remove(true);
}
/**
* Removes from the parent. Cannot be reused if 'markDeleted=true'
*
* @param markDeleted
* @return
*/
public Node remove(boolean markDeleted) {
assert (!_deleted) : "Node already deleted";
assert (this.parent != null);
this.parent.children.remove(this);
this.parent = null;
if (markDeleted) markDeleted();
return this;
}
/**
* Pulls the element, replacing it with its children.
*
* @return
*/
public Node pull() {
if (children != null) {
if (parent != null) {
int thisIndex = parent.children.list().indexOf(this);
parent.addChildren(children.list(), thisIndex); //Put children in parent
}
children.list().clear(); //Remove children.
}
//Remove this
remove();
return this;
}
public XmlToStringWrapper getStringWrapper() throws InvalidMarkupException {
return new XmlToStringWrapper(this);
}
public XmlToStringWrapper getStringWrapper(boolean entityDecode) throws InvalidMarkupException {
return new XmlToStringWrapper(this, entityDecode);
}
/*
public Node mergeTextAndEntities(boolean recursive){
//TODO - complete
return this;
}
public Node splitEntities(boolean recursive){
//TODO - complete
return this;
}
*/
public Node addChild(Node n) {
return addChild(n, -1);
}
public Node addChild(Node n, int atIndex) {
List<Node> l = new ArrayList<Node>();
l.add(n);
addChildren(l, atIndex);
return this;
}
public Node insertBeforeThis(Node n) {
this.parent.addChild(n, parent.children.list().indexOf(this));
return this;
}
public Node insertAfterThis(Node n) {
this.parent.addChild(n, parent.children.list().indexOf(this) + 1);
return this;
}
public Node addChildren(Collection<Node> items) {
return addChildren(items, -1);
}
public Node addChildren(NodeList items) {
return addChildren(items.list(), -1);
}
public Node addChildren(Collection<Node> items, int atIndex) {
if (children == null) {
children = new NodeList(items.size()); //Set up the collection
children.setParent(this);
this.tagType = TagType.Opening;
}
if (atIndex > -1) {
children.list().addAll(atIndex, items); //Add them
} else {
children.list().addAll(items);
}
for (Node n : children.list()) n.parent = this; //Change child references
return this;
}
/**
* Moves, (not copies) children from this node to the specified node.
*
* @param newParent
* @return
*/
public Node moveChildrenTo(Node newParent) {
if (children == null) return this;
newParent.addChildren(children.list()); //Add children to new parent and change references
children.list().clear(); //Remove children from this parent
return this;
}
/**
* Returns a closing tag for this node.
* Throws an exception if this node is not a tag.
*
* @return
* @throws InvalidMarkupException
* @throws InvalidMarkupException
* @throws InvalidMarkupException
*/
public XmlToken getClosingTag() {
assert (this.isTag()) : "Can only be called on tags.";
String tagName = this.getTagNameSilent();
assert (tagName != null) : "getClosingTag() can only be called on XML nodes with non-null tag names.";
XmlToken t = new XmlToken();
t.markup = "</" + tagName + ">";
t.type = TokenType.Tag;
t.setTagName(tagName, false);
t.tagType = TagType.Closing;
return t;
}
/**
* Returns the SLX token for the opening tag or text token.
*
* @return
*/
public SlxToken getSlxToken() {
return new SlxToken(this);
}
public StringBuilder writeXmlTo(StringBuilder sb) {
//Grow or create StringBuilder
if (sb != null) {
sb.ensureCapacity(sb.length() + (int) Math.round(estimateTextSize() * 1.2));
} else {
sb = new StringBuilder((int) Math.round(estimateTextSize() * 1.2));
}
//Write to StringBuilder
super.writeTokenTo(sb);
//Make sure it is an opening tag if it has children.
if (children != null) assert this.isOpening() && this.isTag();
if (this.isTag() && this.isOpening()) {
//Write children
if (children != null) children.writeTo(sb);
//Write closing
sb.append("</");
sb.append(getTagNameSilent());
sb.append(">");
}
return sb;
}
/**
* Recursively estimates required markup size.
*
* @return
*/
private int estimateTextSize() {
int size = 0;
//For the closing tag.
if (isTag() && isOpening()) size += this.getTagNameSilent().length() + 3; //Forces a parse...
//For the opening token/text token.
size += markup != null ? markup.length() : 10; //Main token
//Add children sizes recursively
if (children != null) for (Node n : children.list()) size += n.estimateTextSize();
return size;
}
public String toXmlString(boolean autoIndent) throws InvalidMarkupException {
if (autoIndent) {
return new XmlFormatter(0).format(this);
} else {
return writeXmlTo(null).toString();
}
}
/**
* Returns a list of ancestors, ordered by closest to farthest.
*
* @return
*/
public NodeList ancestors() {
NodeList nl = new NodeList();
Node p = this.parent;
while (p != null) {
nl.list().add(p);
p = p.parent;
}
return nl;
}
public String getClosingTagString() {
return "</" + getTagNameSilent() + ">";
}
public Node deepCopy() {
Node n = new Node();
this.copyTo(n, true);
n.parent = this.parent;
if (this.children != null) {
n.children = this.children.deepCopy();
n.children.setParent(n);
for (Node c : n.children.list()) {
c.parent = n;
}
}
return n;
}
/**
* Needs unit tests
*
* @param className
* @throws InvalidMarkupException
*/
public void addClass(String className) throws InvalidMarkupException {
String cls = (this.get("class"));
if (cls == null) cls = className;
else {
cls = cls.trim() + " " + className.trim();
}
//TODO: add class name(s) validation.
this.set("class", cls);
}
/**
* Needs unit tests
*
* @param className
* @return
* @throws InvalidMarkupException
*/
public boolean hasClass(String className) throws InvalidMarkupException {
String cls = (this.get("class"));
if (cls == null) return false;
String[] classes = cls.split("\\s+");
for (String s : classes) {
if (s.equals(className)) return true;
}
return false;
}
public Node addTo(Node n) {
n.addChild(this);
return this;
}
public Node addTo(Node n, int addAt) {
n.addChild(this, addAt);
return this;
}
public boolean isLastNode() {
return (this.parent.children.list().lastIndexOf(this) == this.parent.children.list().size() - 1);
}
/**
* Returns the root parent (whose parent is null)
*
* @return
*/
public Node rootNode() {
Node p = this;
while (p.parent != null) p = p.parent;
return p;
}
}