/* ************************************************************************
#
# DivConq
#
# http://divconq.com/
#
# Copyright:
# Copyright 2014 eTimeline, LLC. All rights reserved.
#
# License:
# See the license.txt file in the project's top-level directory for details.
#
# Authors:
# * Andy White
#
************************************************************************ */
package divconq.xml;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import divconq.lang.Memory;
import divconq.lang.op.FuncResult;
import divconq.struct.CompositeStruct;
import divconq.struct.CompositeParser;
import divconq.util.StringUtil;
/**
* Represents a XML element and contains all the information that the
* original had. Attributes may be set, accessed and deleted. Child elements may
* be set accessed or deleted. This can be converted back to XML, along with its
* children, in a formatted or unformatted string.
*/
public class XElement extends XNode {
protected String tagName = null;
protected int line = 0;
protected int col = 0;
protected Map<String, String> attributes = null;
protected List<XNode> children = null;
protected String comment = null;
/**
* constructor specifying the name with an optional array
* of objects to be added as child elements
*
* @param tag the name of the tag
* @param children
* an array of objects to be added as children
*/
public XElement(String tag, Object... children) {
this.tagName = tag;
for (int i = 0; i < children.length; i++) {
Object obj = children[i];
if (obj instanceof XNode)
this.add((XNode) obj);
else if (obj instanceof XAttribute)
this.setAttribute(((XAttribute) obj).getName(), ((XAttribute) obj).getRawValue());
else
this.add(obj.toString());
}
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#clone()
*/
/*
* TODO public Object clone() throws CloneNotSupportedException {
* TaggedElement newElement = (TaggedElement)super.clone(); if (attributes
* != null) { newElement.attributes = new
* HashMap<String,String>(attributes); } if (elements != null) {
* newElement.elements = new ArrayList<Element>(); Iterator<Element> it =
* elements.iterator(); while (it.hasNext()) {
* newElement.elements.add((Element)it.next().clone()); } } return
* newElement; }
*/
public XElement(XMLStreamReader xmlStreamReader, boolean keepwhitespace) {
this.tagName = xmlStreamReader.getLocalName();
for (int a = 0; a < xmlStreamReader.getAttributeCount(); a++)
this.setAttribute(xmlStreamReader.getAttributeLocalName(a), xmlStreamReader.getAttributeValue(a));
try {
while (xmlStreamReader.hasNext()) {
int n = xmlStreamReader.next();
switch (n) {
case XMLStreamConstants.START_ELEMENT:
this.add(new XElement(xmlStreamReader, keepwhitespace));
break;
case XMLStreamConstants.END_ELEMENT:
return;
case XMLStreamConstants.SPACE:
case XMLStreamConstants.CHARACTERS:
String str = xmlStreamReader.getText();
if (!keepwhitespace) {
str = StringUtil.stripWhitespacePerXml(str);
}
// this is not always good - see if we can do it anyway
if (StringUtil.isEmpty(str))
break;
XText text = new XText();
text.setRawValue(str);
this.add(text);
break;
/*
case XMLStreamConstants.ATTRIBUTE:
for (int a = 0; a < xmlStreamReader.getAttributeCount(); a++) {
this.setAttribute(xmlStreamReader.getAttributeLocalName(a), xmlStreamReader.getAttributeValue(a));
}
break;
*/
case XMLStreamConstants.CDATA:
String str2 = xmlStreamReader.getText();
if (!keepwhitespace) {
str2 = str2.trim();
if (StringUtil.isEmpty(str2))
break;
}
XText text2 = new XText();
text2.setValue(str2, true);
this.add(text2);
break;
}
}
}
catch (XMLStreamException x) {
// TODO Auto-generated catch block
x.printStackTrace();
}
}
/**
* gets the element name
*
* @return the element name
*/
public String getName() {
return this.tagName;
}
public void setName(String v) {
this.tagName = v;
}
/**
* sets an attribute of this element
*
* @param name
* the name of the attribute to be set
* @param value
* the value of the attribute to be set
*/
public void setAttribute(String name, String value) {
if (this.attributes == null)
this.attributes = new HashMap<String, String>();
this.attributes.put(name, XNode.quote(value));
}
public void setRawAttribute(String name, String value) {
if (this.attributes == null)
this.attributes = new HashMap<String, String>();
this.attributes.put(name, value);
}
public XElement withAttribute(String name, String value) {
if (this.attributes == null)
this.attributes = new HashMap<String, String>();
this.attributes.put(name, XNode.quote(value));
return this;
}
/**
* gets the specified attribute of this element
*
* @param name
* the name of the attribute to get
* @return the value of the attribute
*/
public String getAttribute(String name) {
return (this.attributes == null ? null : XNode.unquote(this.attributes.get(name)));
}
public String getRawAttribute(String name) {
//return (this.attributes == null ? null : XNode.unquote(this.attributes.get(name)));
return (this.attributes == null ? null : this.attributes.get(name));
}
/**
* gets the specified attribute of this element but returns given default
* value if the attribute does not exist
*
* @param name
* the name of the attribute to get
* @param defaultValue
* the value to be returned if the attribute doesn't exist
* @return the value of the attribute
*/
public String getAttribute(String name, String defaultValue) {
String result = this.getAttribute(name);
return result == null ? defaultValue : result;
}
/**
* finds out whether an attribute exists
*
* @param name
* the name of the attribute to look for
* @return whether the attribute exists in this element
*/
public boolean hasAttribute(String name) {
return this.attributes == null ? false : this.attributes.containsKey(name);
}
/**
* finds out whether this element has any attributes
*
* @return whether this element has any attributes
*/
public boolean hasAttributes() {
return this.attributes != null && !this.attributes.isEmpty();
}
/**
* gets the attributes in the form of an indexed table
*
* @return the attribute table for this element
*/
public Map<String, String> getAttributes() {
if (this.attributes == null)
this.attributes = new HashMap<String, String>();
return this.attributes;
}
/**
* removes the named attribute
*
* @param name Name of attribute to remove
*/
public void removeAttribute(String name) {
if (this.attributes != null)
this.attributes.remove(name);
}
/**
* gets the number of child elements this element has
*
* @return the number of child elements this element has
*/
public int children() {
return this.children == null ? 0 : this.children.size();
}
/**
* finds out whether this element has any child elements
*
* @return whether this element has any child elements
*/
public boolean hasChildren() {
return this.children() != 0;
}
/**
* removes the children from this element
*
*/
public void clearChildren() {
if (children != null)
children.clear();
}
/**
* adds a child to the end of this element
*
* @param element
* the child element to be added
*/
public void add(XNode element) {
this.add(-1, element);
}
public XElement with(XNode v) {
this.add(-1, v);
return this;
}
/**
* inserts a child into this element. If the index is out of range, the
* child is added at the end
*
* @param index
* the location to insert the child
* @param element
* the child element to be added
*/
public void add(int index, XNode element) {
if (this.children == null)
this.children = new ArrayList<XNode>();
if ((index < 0) || (index >= this.children.size()))
this.children.add(element);
else
this.children.add(index, element);
}
/**
* adds a child to the end of this element
*
* @param string
* the child element to be added
*/
public void add(String string) {
this.add(new XText(string));
}
public XElement withText(String v) {
this.add(new XText(v));
return this;
}
public XElement withCData(String v) {
this.add(new XText(true, v));
return this;
}
/**
* inserts a child into this element If the index is out of range, the child
* is added at the end
*
* @param index
* the location to insert the child
* @param string
* the child element to be added
*/
public void add(int index, String string) {
this.add(index, new XText(string));
}
public void append(char c) {
if ((this.children != null) && (this.children.size() > 0)) {
XNode node = this.children.get(this.children.size() - 1);
if (node instanceof XText) {
XText t = (XText) node;
if (!t.getCData()) {
t.append(c);
return;
}
}
}
this.add(c + "");
}
public void append(String s) {
if ((this.children != null) && (this.children.size() > 0)) {
XNode node = this.children.get(this.children.size() - 1);
if (node instanceof XText) {
XText t = (XText) node;
if (!t.getCData()) {
t.append(s);
return;
}
}
}
this.add(s);
}
public void appendRaw(String s) {
if ((this.children != null) && (this.children.size() > 0)) {
XNode node = this.children.get(this.children.size() - 1);
if (node instanceof XText) {
XText t = (XText) node;
if (t.getCData()) {
t.appendEntity(s);
return;
}
}
}
this.withCData(s);
}
/**
* gets a child from the specified place in this element
*
* @param i
* the index where the child is to be added
* @return the specified child
*/
public XNode getChild(int i) {
if ((i < 0) || (i >= this.children()))
return null;
return this.children.get(i);
}
/**
* replaces a child element with the one given
*
* @param index
* the child element number to replace
* @param newElement
* the new element to replace the old one
*/
public void replace(int index, XNode newElement) {
if (this.children == null)
this.children = new ArrayList<XNode>();
if (index >= this.children.size())
this.children.add(newElement);
else
this.children.set(index, newElement);
}
/**
* replaces all children with the children of the provided element
*
* @param source
* the new element providing the source of the children
*/
public void replaceChildren(XElement source) {
this.children = new ArrayList<XNode>();
if (source.hasChildren())
this.children.addAll(source.children);
}
public void replaceAttributes(XElement source) {
this.attributes = new HashMap<String, String>();
if (source.hasAttributes())
this.attributes.putAll(source.attributes);
}
public void replace(XElement source) {
this.tagName = source.tagName;
this.comment = source.comment;
this.replaceChildren(source);
this.replaceAttributes(source);
}
/**
* Removes a child from this element
*
* @param index
* the index of the child to be removed
* @return whether the child was found and removed or not
*/
public XNode remove(int index) {
if (this.children == null)
return null;
if (index >= 0 && index < this.children.size())
return this.children.remove(index);
return null;
}
/**
* Removes a child from this element
*
* @param element
* the child to be removed
* @return whether the child was found and removed or not
*/
public boolean remove(XNode element) {
if (element == null)
return true;
if (this.children != null)
return this.children.remove(element);
return false;
}
@Override
public XNode deepCopy() {
XElement copy = new XElement(this.tagName);
copy.line = this.line;
copy.col = this.col;
copy.comment = this.comment;
if (this.attributes != null) {
copy.attributes = new HashMap<>();
for (Entry<String, String> entry : this.attributes.entrySet())
copy.attributes.put(entry.getKey(), entry.getValue());
}
if (this.children != null) {
copy.children = new ArrayList<XNode>();
for (XNode entry : this.children)
copy.children.add(entry.deepCopy());
}
return copy;
}
/**
* Finds a named child tagged element. If there is no such child, null is
* returned.
*
* @param name
* the name of the child of this TaggedElement to find
* @return the name of the found element or null if not found
*/
public XElement find(String... name) {
if (this.children != null)
for (int i = 0; i < this.children.size(); i++) {
XNode element = this.children.get(i);
if (element instanceof XElement) {
for (int n = 0; n < name.length; n++)
if (((XElement) element).getName().equals(name[n]))
return (XElement) element;
}
}
return null;
}
public XElement findId(String id) {
if (id == null)
return null;
if (this.attributes != null)
if (id.equals(this.getAttribute("id")) || id.equals(this.getAttribute("Id")) || id.equals(this.getAttribute("ID")))
return this;
if (this.children != null) {
for (XNode n : this.children) {
if (n instanceof XElement) {
XElement match = ((XElement)n).findId(id);
if (match != null)
return match;
}
}
}
return null;
}
public XElement findParentOfId(String id) {
return findParentOfId(id, null);
}
public XElement findParentOfId(String id, XElement parent) {
if (id == null)
return null;
if (this.attributes != null)
if (id.equals(this.getAttribute("id")) || id.equals(this.getAttribute("Id")) || id.equals(this.getAttribute("ID")))
return parent;
if (this.children != null) {
for (XNode n : this.children) {
if (n instanceof XElement) {
XElement match = ((XElement)n).findParentOfId(id, this);
if (match != null)
return match;
}
}
}
return null;
}
/**
* A way to select child or sub child elements similar to XPath but lightweight.
* Cannot select values or attributes, just elements. * is supported to match
* all elements at a given level.
*
* For example: "Toys/Toy" called on "<Person>" means select all Toy elements
* inside of the Toys child element (child of Person).
*
* Cannot go up levels, or back to root. Do not start with a slash as in "/People".
*
* @param path a backslash delimited string
* @return list of all matching elements, or empty list if no match
*/
public List<XElement> selectAll(String path) {
List<XElement> matches = new ArrayList<XElement>();
this.selectAll(path, matches);
return matches;
}
/**
* Internal, recursive search used by selectAll
*
* @param path a backslash delimited string
* @param matches list of all matching elements, or empty list if no match
*/
protected void selectAll(String path, List<XElement> matches) {
if (!this.hasChildren())
return;
int pos = path.indexOf('/');
// go back to root not supported
if (pos == 0)
return;
String name = null;
if (pos == -1) {
name = path;
path = null;
}
else {
name = path.substring(0, pos);
path = path.substring(pos + 1);
}
// TODO add filter per XPath - [@n = f]
for (XNode n : this.children) {
if (n instanceof XElement) {
if ("*".equals(name) || ((XElement)n).getName().equals(name)) {
if (pos == -1)
matches.add((XElement)n);
else
((XElement)n).selectAll(path, matches);
}
}
}
}
/**
* A way to select text of a child or sub child elements similar to XPath but lightweight.
* '*' is supported to match
* all elements at a given level. Returns only the first match.
*
* For example: "Toys/Toy" called on "<Person>" means return the text of first Toy element
* inside of the Toys child element (child of Person).
*
* Cannot go up levels, or back to root. Do not start with a slash as in "/People".
*
* @param path a backslash delimited string
* @return text of first matching element, or null if no match
*/
public String selectFirstText(String path) {
XElement first = this.selectFirst(path);
if (first != null)
return first.getText();
return null;
}
/**
* A way to select text of a child or sub child elements similar to XPath but lightweight.
* '*' is supported to match
* all elements at a given level. Returns only the first match.
*
* For example: "Toys/Toy" called on "<Person>" means return the text of first Toy element
* inside of the Toys child element (child of Person).
*
* Cannot go up levels, or back to root. Do not start with a slash as in "/People".
*
* @param path a backslash delimited string
* @param def default text if none found
* @return text of first matching element, or null if no match
*/
public String selectFirstText(String path, String def) {
XElement first = this.selectFirst(path);
if (first != null) {
String t = first.getText();
if (StringUtil.isNotEmpty(t))
return t;
}
return def;
}
/**
* A way to select text of a child or sub child elements similar to XPath but lightweight.
* '*' is supported to match
* all elements at a given level. Returns only the first match.
*
* For example: "Toys/Toy" called on "<Person>" means return the text of first Toy element
* inside of the Toys child element (child of Person).
*
* Cannot go up levels, or back to root. Do not start with a slash as in "/People".
*
* @param path a backslash delimited string
* @param def default object to return if path not found
* @return text of first matching element, or null if no match
*/
public Object selectFirstValue(String path, Object def) {
XElement first = this.selectFirst(path);
if (first != null) {
String t = first.getText();
if (StringUtil.isNotEmpty(t))
return t;
}
return def;
}
/**
* A way to select child or sub child elements similar to XPath but lightweight.
* Cannot select values or attributes, just elements. * is supported to match
* all elements at a given level. Returns only the first match.
*
* For example: "Toys/Toy" called on "<Person>" means return first Toy element
* inside of the Toys child element (child of Person).
*
* Cannot go up levels, or back to root. Do not start with a slash as in "/People".
*
* @param path a backslash delimited string
* @return first matching element, or null if no match
*/
public XElement selectFirst(String path) {
if (!this.hasChildren())
return null;
int pos = path.indexOf('/');
// go back to root not supported
if (pos == 0)
return null;
String name = null;
if (pos == -1) {
name = path;
path = null;
}
else {
name = path.substring(0, pos);
path = path.substring(pos + 1);
}
// TODO add filter per XPath - [@n = f]
for (XNode n : this.children) {
if (n instanceof XElement) {
if ("*".equals(name) || ((XElement)n).getName().equals(name)) {
if (pos == -1)
return (XElement)n;
else {
XElement r = ((XElement)n).selectFirst(path);
if (r != null)
return r;
}
}
}
}
return null;
}
/**
* Finds the index of a named child tagged element. If there is no such
* child, -1 is returned.
*
* @param name
* the name of the child of this TaggedElement to find
* @return the index of the found element or -1 if not found
*/
public int findIndex(String name) {
if (this.children != null)
for (int i = 0; i < this.children.size(); i++) {
XNode node = this.children.get(i);
if (node instanceof XElement)
if (((XElement) node).getName().equals(name))
return i;
}
return -1;
}
/**
* sets the list of children of this element. This method replaces the
* current children.
*
* @param elements
* the new list of children
*/
public void setElements(List<XNode> elements) {
this.children = elements;
}
/**
* gets a list of the children of this element. This method always returns a
* List even if it is empty.
*
* @return the List containing the children of this element
*/
public Collection<XNode> getChildren() {
if (this.children == null)
this.children = new ArrayList<XNode>();
return this.children;
}
public int getChildCount() {
if (this.children == null)
return 0;
return this.children.size();
}
/**
* sets the XML source code location information for this element
*
* @param line
* the line number of the start tag
* @param col
* the column number of the start tag
*/
public void setLocation(int line, int col) {
this.line = line;
this.col = col;
}
/**
* gets the XML source code line number where this element was declared.
* This number will be 0 if it was never set.
*
* @return the XML source code line number of this element's declaration
*/
public int getLine() {
return this.line;
}
/**
* gets the XML source code cloumn number where this element was declared.
* This number will be 0 if it was never set.
*
* @return the XML source code column number of this element's declaration
*/
public int getCol() {
return this.col;
}
/**
* get the comment associated with this element
*
* @return this element's comment
*/
public String getComment() {
return this.comment;
}
/**
* set this element's comment
*
* @param comment
* the comment to be stored
*/
public void setComment(String comment) {
this.comment = comment;
}
/**
*
* @return the text contained in this element if any, else null
*/
public String getText() {
if (!this.hasChildren())
return null;
// TODO improve to support multiple CDATA sections in one element
XNode f = this.children.get(0);
if (f instanceof XText)
return ((XText)f).getValue();
return null;
}
// TODO return false if only white space
public boolean hasText() {
if (!this.hasChildren())
return false;
// TODO improve to support multiple CDATA sections in one element
XNode f = this.children.get(0);
if (f instanceof XText)
return true;
return false;
}
// returns Value attribute if present, else returns text
public String getValue() {
if (this.hasAttribute("Value"))
return this.getAttribute("Value");
return this.getText();
}
public boolean hasValue() {
if (this.hasAttribute("Value"))
return StringUtil.isNotEmpty(this.getAttribute("Value"));
return this.hasText();
}
/**
* assumes that the text content of this element is escaped xml,
* reads and parses the text content and returns the root element
* from that content
*
* @param keepwhitespace don't strip white space when parsing the content of element
* @return root xml element for parsed content or null
*/
public FuncResult<XElement> toXml(boolean keepwhitespace) {
if (!this.hasChildren()) {
FuncResult<XElement> res = new FuncResult<XElement>();
res.errorTr(244);
return res;
}
XNode f = this.children.get(0);
if (f instanceof XText)
return XmlReader.parse(((XText)f).getValue(), keepwhitespace);
FuncResult<XElement> res = new FuncResult<XElement>();
res.errorTr(245);
return res;
}
/**
* assumes the text content of this element is Json, reads and
* parses the text content
*
* @return struct or null
*/
public CompositeStruct toStruct() {
if (!this.hasChildren())
return null;
XNode f = this.children.get(0);
if (f instanceof XText)
return CompositeParser.parseJson(((XText)f).getValue()).getResult();
return null;
}
/**
*
* @return the first child node or null
*/
public XNode getFirstChild() {
if (this.hasChildren())
return this.children.get(0);
return null;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return this.toString(true);
}
/**
* returns just the tag start, not the content or children or the tag end.
* useful for debugging
*
* @return tag start in xml syntax
*/
public String toLocalString() {
StringBuffer sb = new StringBuffer();
// Put the opening tag out
sb.append("<" + this.tagName);
// Write the attributes out
if (this.attributes != null)
for (Map.Entry<String, String> entry : this.attributes.entrySet()) {
sb.append(" " + entry.getKey() + "=");
sb.append("\"" + entry.getValue() + "\"");
}
sb.append(">");
return sb.toString();
}
public String toInnerString() {
return this.toInnerString(true);
}
public String toInnerString(boolean formatted) {
StringBuffer sb = new StringBuffer();
// write out the closing tag or other elements
boolean formatThis = formatted;
if (this.hasChildren()) {
for (XNode element : this.children) {
formatThis = (element instanceof XText) ? false : formatted;
element.toString(sb, formatThis, 1);
}
}
return sb.toString();
}
/* (non-Javadoc)
* @see divconq.xml.XNode#toString(java.lang.StringBuffer, boolean, int)
*/
@Override
protected StringBuffer toString(StringBuffer sb, boolean formatted, int level) {
// Add leading newline and spaces, if necessary
if (formatted && level > 0) {
sb.append("\n");
for (int i = level; i > 0; i--)
sb.append("\t");
}
// Put the opening tag out
sb.append("<" + this.tagName);
// Write the attributes out
if (this.attributes != null)
for (Map.Entry<String, String> entry : this.attributes.entrySet()) {
sb.append(" " + entry.getKey() + "=");
sb.append("\"" + entry.getValue() + "\"");
}
// write out the closing tag or other elements
boolean formatThis = formatted;
boolean nontext = false;
if (!this.hasChildren()) {
sb.append(" /> ");
}
else {
sb.append(">");
for (XNode node : this.children) {
formatThis = (node instanceof XText) ? false : formatted;
if (!(node instanceof XText))
nontext = true;
node.toString(sb, formatThis, level + 1);
}
// Add leading newline and spaces, if necessary
if (formatThis || nontext) {
sb.append("\n");
for (int i = level; i > 0; i--)
sb.append("\t");
}
// Now put the closing tag out
sb.append("</" + this.tagName + "> ");
}
return sb;
}
/* (non-Javadoc)
* @see divconq.xml.XNode#toMemory(divconq.lang.Memory, boolean, int)
*/
@Override
protected void toMemory(Memory sb, boolean formatted, int level) {
// Add leading newline and spaces, if necessary
if (formatted && level > 0) {
sb.write("\n");
for (int i = level; i > 0; i--)
sb.write("\t");
}
// Put the opening tag out
sb.write("<" + this.tagName);
// Write the attributes out
if (this.attributes != null)
for (Map.Entry<String, String> entry : this.attributes.entrySet()) {
sb.write(" " + entry.getKey() + "=");
sb.write("\"" + entry.getValue() + "\"");
}
// write out the closing tag or other elements
boolean formatThis = formatted;
boolean nontext = false;
if (!this.hasChildren()) {
sb.write(" /> ");
}
else {
sb.write(">");
for (XNode node : this.children) {
formatThis = (node instanceof XText) ? false : formatted;
if (!(node instanceof XText))
nontext = true;
node.toMemory(sb, formatThis, level + 1);
}
// Add leading newline and spaces, if necessary
if (formatThis || nontext) {
sb.write("\n");
for (int i = level; i > 0; i--)
sb.write("\t");
}
// Now put the closing tag out
sb.write("</" + this.tagName + "> ");
}
}
}