package javaforce;
import java.lang.reflect.*;
import java.io.*;
import java.util.*;
import java.awt.*;
import javax.swing.tree.*;
import javax.swing.event.*;
/**
* XML is a TreeModel data model that encapsules a complete XML file.
Each
* XML tag (element) is treated as a node in the tree. Once read() it can be
* viewed and edited with a JTree. Then you can write() it back to a file. XML
* will monitor changes made and update nodes as needed. The read() functions
* include a callback interface so you can further tweak the layout of the XML
* tree.
Typical XML Tag:
<name [attributes...]> content |
* children </name>
Singleton XML Tag: (no children)
<name
* [attributes...] />
Caveats:
Only leaf nodes can contain actual
* data (content) (in other words @XmlMixed is not supported).
* Mixed tags are read, but when written the
* content is lost.
There must be only one root tag.
Support for the
* standard XML header is provided (see header).
readClass() and
* writeClass() support : int, short, byte, float, double, boolean, String,
* Color and Custom Classes.
Arrays of any of these.
All classes and
* fields MUST be public. static and transient members are skipped. No special
* annotations are required.
*/
public class XML implements TreeModelListener {
private DefaultTreeModel treemodel;
private boolean useContentForNameGlobal = false;
private boolean fireEvents = true;
private boolean ignoreEvents = false;
private class XMLTagPart {
private String content;
private String attrs;
public XMLTagPart() {
content = "";
attrs = "";
}
}
/**
* XMLEvent is an interface for a callback handler used during XML loading.
*/
public interface XMLEvent {
public void XMLTagAdded(XMLTag tag);
public void XMLTagRenamed(XMLTag tag);
};
/**
* XMLAttr is one attribute that is listed in each XML tag.
*/
public class XMLAttr {
public String name, value;
public XMLAttr() {
name = "";
value = "";
}
};
/**
* XMLTag is one node in the tree that represents one XML element or 'tag'.
*
* @param name the XML tag name
* @param attrs an ArrayList of XMLAttr
* @param uname the unique name of the tag. Usually equals name unless another
* child with the same parent has the same name. JTree uses uname to display
* the tags.
* @param content the data within the tags head/tail
* @param isLeaf set to force JTree to view node as a leaf
* @param isNotLeaf set to force JTree to view node that is expandable (even
* if it has no children)
* @param isReadOnly ignores edits from JTree
* @param useContentForName causes JTree to use content for display
*/
public class XMLTag extends DefaultMutableTreeNode {
public String name = "";
public ArrayList
*/
public XMLTag header = new XMLTag();
/**
* The root tag.
*/
public XMLTag root = new XMLTag();
/**
* Constructs a new XML object.
*/
public XML() {
treemodel = new DefaultTreeModel(root);
treemodel.addTreeModelListener(this);
treemodel.setRoot(root);
}
/**
* Returns the TreeModel that can be passed to JTree constructor.
*/
public TreeModel getTreeModel() {
return treemodel;
}
public DefaultTreeModel getDefaultTreeModel() {
return treemodel;
}
private final int XML_OPEN = 1;
private final int XML_DATA = 2;
private final int XML_CLOSE = 3;
private final int XML_SINGLE = 4;
private int type, nexttype;
private XMLTagPart readtag(Reader rdr) {
boolean quote = false, isAttrs = false;
int ich;
char ch;
XMLTagPart tag = new XMLTagPart();
type = XML_DATA;
if (nexttype != -1) {
type = nexttype;
nexttype = -1;
}
while (true) {
try {
ich = rdr.read();
} catch (Exception e) {
break;
}
if (ich == -1) {
break;
}
ch = (char) ich;
switch (type) {
case XML_OPEN:
case XML_CLOSE:
case XML_SINGLE:
if (ch == '\"') {
quote = !quote;
}
if (!quote) {
if (ch == '/') {
if (tag.content.length() == 0) {
type = XML_CLOSE;
} else {
type = XML_SINGLE;
}
continue;
}
if (ch == '>') {
break;
}
}
if ((ch == ' ') && (!isAttrs)) {
isAttrs = true;
}
if (isAttrs) {
tag.attrs += ch;
} else {
tag.content += ch;
}
continue;
case XML_DATA:
if ((ch == '<') && (!quote)) {
if (tag.content.length() > 0) {
nexttype = XML_OPEN;
break;
}
type = XML_OPEN;
continue;
}
if (ch == '\"') {
if (quote) {
quote = false;
} else {
quote = true;
}
}
tag.content += ch;
continue;
}
break;
}
if (tag.content.length() == 0) {
return null; //EOF
}
if (type == XML_DATA) {
tag.content = decodeSafe(tag.content);
}
return tag;
}
private void string2attrs(XMLTag tag, String attrs) {
//search for name="value"
char ca[] = attrs.toCharArray();
XMLAttr attr;
String name, value;
int length = attrs.length();
int ep;
tag.attrs.clear();
for (int a = 0; a < length; a++) {
if (ca[a] == ' ') {
continue; //skip spaces
}
ep = attrs.indexOf('=', a);
if (ep == -1) {
return;
}
name = "";
for (int b = a; b < ep; b++) {
name += ca[b];
}
a = ep + 1;
value = "";
if (ca[a] == '\"') {
a++;
ep = attrs.indexOf('\"', a);
if (ep == -1) {
return;
}
for (int b = a; b < ep; b++) {
value += ca[b];
}
a = ep + 1;
} else if (ca[a] == '\'') {
a++;
ep = attrs.indexOf('\'', a);
if (ep == -1) {
return;
}
for (int b = a; b < ep; b++) {
value += ca[b];
}
a = ep + 1;
} else {
ep = attrs.indexOf(' ', a);
if (ep == -1) {
ep = length - 1;
}
if (ep <= a) {
return;
}
for (int b = a; b < ep; b++) {
value += ca[b];
}
a = ep + 1;
}
attr = new XMLAttr();
attr.name = name;
attr.value = value;
tag.attrs.add(attr);
}
}
private void setuname(XMLTag tag) {
XMLTag parent = tag.getParent();
String uname = tag.name;
XMLAttr attr;
for (Iterator i = tag.attrs.iterator(); i.hasNext();) {
attr = (XMLAttr) i.next();
if (attr.name.equalsIgnoreCase("name")) {
uname = attr.value;
break;
}
}
String orguname = uname;
if (parent == null) {
tag.uname = uname;
changedTag(tag);
return;
}
boolean ok;
int idx = 1;
int size = parent.getChildCount();
while (true) {
ok = true;
for (int a = 0; a < size; a++) {
XMLTag child = (XMLTag) parent.getChildAt(a);
if (child == tag) {
continue;
}
if (child.getName().equalsIgnoreCase(uname)) {
ok = false;
break;
}
}
if (ok) {
break;
}
uname = orguname + idx;
idx++;
}
tag.uname = uname;
changedTag(tag);
}
/**
* Reads the entire tree from a XML file from filename. No call handler is
* used.
*
* @param filename name of file to load XML data from
*/
public boolean read(String filename) {
return read(filename, null);
}
/**
* Reads the entire tree from a XML file from filename.
*
* @param filename name of file to load XML data from
* @param event callback handler to process each loaded XML tag
*/
public boolean read(String filename, XMLEvent event) {
FileInputStream fis;
boolean ret;
try {
fis = new FileInputStream(filename);
ret = read(fis, event);
fis.close();
} catch (Exception e) {
JFLog.log(e);
return false;
}
return ret;
}
/**
* Reads the entire tree from a XML file from the InputStream. No callback
* handler is used.
*
* @param in InputStream to load XML data from
*/
public boolean read(InputStream in) {
return read(in, null);
}
/**
* Reads the entire tree from a XML file from the InputStream.
*
* @param in InputStream to load XML data from
* @param event callback handler to process each loaded XML tag
*/
public boolean read(InputStream is, XMLEvent event) {
BufferedReader bris;
try {
bris = new BufferedReader(new InputStreamReader(is, "UTF-8"));
} catch (Exception e) {
JFLog.log(e);
return false;
}
this.event = event;
deleteAll();
type = XML_DATA;
nexttype = -1;
XMLTagPart tagpart;
XMLTag tag = null, newtag;
boolean bRoot = false;
boolean bHeader = false;
while (true) {
tagpart = readtag(bris);
if (tagpart == null) {
break;
}
switch (type) {
case XML_OPEN:
if (tagpart.content.startsWith("?xml")) {
if (bHeader) {
JFLog.log("XML:Multiple headers found");
return false;
} //already read the XML header
header.name = tagpart.content;
header.uname = header.name;
string2attrs(header, tagpart.attrs);
if (event != null) {
event.XMLTagAdded(header);
}
break;
}
//no break
case XML_SINGLE:
if (tag == null) {
//root tag
if (bRoot) {
JFLog.log("XML:Multiple root tags found");
return false;
} //already found a root tag
bRoot = true;
root.name = tagpart.content;
root.uname = root.name;
string2attrs(root, tagpart.attrs);
if (event != null) {
event.XMLTagAdded(root);
}
changedTag(root);
tag = root;
} else {
newtag = new XMLTag();
newtag.name = tagpart.content;
string2attrs(newtag, tagpart.attrs);
addTag(tag, newtag);
if (type == XML_SINGLE) {
newtag.isSingle = true;
} else {
tag = newtag;
}
}
break;
case XML_CLOSE:
if (tag == null) {
JFLog.log("XML:tag closed but never opened");
return false;
} //bad xml file
if (!tagpart.content.equalsIgnoreCase(tag.name)) {
JFLog.log("XML:tag closed doesn't match open");
return false;
} //unmatched closing tag
tag = (XMLTag) tag.getParent();
break;
case XML_DATA:
if (tag == null) {
continue; //could happen after header and before root tag
}
tag.content += tagpart.content;
break;
}
}
if (tag != null) {
JFLog.log("XML:tag left open");
return false;
} //tag left open
return true;
}
private String attrs2string(XMLTag tag) {
XMLAttr attr;
int size = tag.attrs.size();
String str = "", tmp;
for (int a = 0; a < size; a++) {
attr = tag.attrs.get(a);
tmp = " " + attr.name + "=\"" + attr.value + "\"";
str += tmp;
}
return str;
}
private void writestr(OutputStream out, String str) {
try {
out.write(str.getBytes("UTF-8"));
} catch (Exception e) {
}
}
private int indent;
private void writetag(OutputStream out, XMLTag tag) {
String tmp;
tmp = "";
for (int a = 0; a < indent; a++) {
tmp += ' ';
}
writestr(out, tmp);
int size = tag.getChildCount();
String attrs;
if (size > 0) {
//write open tag w/ attrs + content
attrs = attrs2string(tag);
tmp = "<" + tag.name + attrs + ">\n";
writestr(out, tmp);
indent += 2;
//write children
for (int a = 0; a < size; a++) {
writetag(out, (XMLTag) tag.getChildAt(a));
}
//write close tag
indent -= 2;
tmp = "";
for (int a = 0; a < indent; a++) {
tmp += ' ';
}
writestr(out, tmp);
tmp = "" + tag.name + ">\n";
writestr(out, tmp);
} else {
attrs = attrs2string(tag);
if (tag.isSingle) {
tmp = "<" + tag.name + attrs + "/>\n";
} else {
tmp = "<" + tag.name + attrs + ">" + encodeSafe(tag.content) + "" + tag.name + ">\n";
}
writestr(out, tmp);
}
}
/**
* Writes the entire tree as a XML file to the filename.
*/
public boolean write(String filename) {
FileOutputStream fos;
boolean ret;
try {
fos = new FileOutputStream(filename);
ret = write(fos);
fos.close();
} catch (Exception e) {
JFLog.log(e);
return false;
}
return ret;
}
/**
* Writes the entire tree as a XML file to the OutputStream.
*/
public boolean write(OutputStream os) {
BufferedOutputStream bos = new BufferedOutputStream(os);
String tmp, attrs;
if (root.name.length() == 0) {
return false;
}
if (header.name.length() > 0) {
attrs = attrs2string(header);
tmp = "<" + header.name + attrs + ">\n";
writestr(bos, tmp);
}
//write root header
attrs = attrs2string(root);
tmp = "<" + root.name + attrs + ">\n";
writestr(bos, tmp);
int size = root.getChildCount();
indent = 2;
for (int a = 0; a < size; a++) {
writetag(bos, (XMLTag) root.getChildAt(a));
}
//write root tail
tmp = "" + root.name + ">\n";
writestr(bos, tmp);
try {
bos.flush();
} catch (Exception e) {
} //must flush or it's lost
return true;
}
private void clearTag(XMLTag tag) {
tag.name = "";
tag.attrs = new ArrayList
*
* @param rootName = name to assign to root tag.
*/
public void readClass(String rootName, Object obj) {
this.deleteAll();
root.setName(rootName);
readClass(root, obj);
}
public void setEventListener(XMLEvent event) {
this.event = event;
}
private String encodeSafe(String in) {
return in.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); //order matters here
}
private String decodeSafe(String in) {
return in.replaceAll(">", ">").replaceAll("<", "<").replaceAll("&", "&");
}
//interface TreeModelListener
public void treeNodesChanged(TreeModelEvent e) {
if (ignoreEvents) return;
XMLTag parent = (XMLTag) (e.getTreePath().getLastPathComponent());
int indices[] = e.getChildIndices();
if (indices == null || indices.length == 0) {
return;
}
/*
* If the event lists children, then the changed
* node is the child of the node we've already
* gotten. Otherwise, the changed node and the
* specified node are the same.
*/
int index = indices[0];
XMLTag tag = (XMLTag) (parent.getChildAt(index));
if (tag.isReadOnly) {
return;
}
if (tag.getUserObject() == null) {
return;
}
fireEvents = false; //prevent inf loop (setName will call changedTag which calls treemodel.nodeChanged)
tag.setName(tag.getUserObject().toString());
fireEvents = true;
if (event != null) {
event.XMLTagRenamed(tag);
}
}
public void treeNodesInserted(TreeModelEvent e) {
}
public void treeNodesRemoved(TreeModelEvent e) {
}
public void treeStructureChanged(TreeModelEvent e) {
}
};