package javaforce;
import java.io.*;
import java.util.ArrayList;
import java.util.Iterator;
import javax.swing.tree.TreeModel;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;
import javax.swing.event.TreeModelListener;
import javax.swing.event.TreeModelEvent;
import java.lang.reflect.Field;
import java.awt.Color;
/** 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.<br>
* Typical XML Tag:<br>
* <name [arguments...]> content | children </name><br>
* Singleton XML Tag: (no children)<br>
* <name [arguments...] /><br>
* Cavets:<br>
* Only leaf nodes can contain actual data (content) (in other words @XmlMixed is not fully supported).<br>
* Mixed tags are read, but when writen the content is lost.
* There must be only one root tag.<br>
* Support for the standard XML header is provided (see header).<br>
*/
public class XML implements TreeModelListener {
private DefaultTreeModel treemodel;
private class XMLTagPart {
private String content;
private String args;
public XMLTagPart() {
content = "";
args = "";
}
}
/** 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 args an ArrayList of XMLAttr (arguments)
* @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
*/
public class XMLTag extends DefaultMutableTreeNode {
public String name = "";
public ArrayList<XMLAttr> args;
public String uname = ""; //unique name (usually same as name)
public String content = "";
public boolean isSingle = false;
public boolean isNotLeaf = false;
public boolean isLeaf = false;
public boolean isReadOnly = false;
/** Constructs a new XMLTag
*/
public XMLTag() {
args = new ArrayList<XMLAttr>();
}
/** Returns the unique name of the tag.
*/
public String toString() {
return getName();
}
/** Returns the parent of the tag.
* This method just overrides the default method but returns XMLTag.
*/
public XMLTag getParent() {
if (this == treemodel.getRoot()) return null; //in case setRoot() moved the root up
return (XMLTag)super.getParent();
}
/** Returns true if the node is a leaf.
* This method just overrides the default method and allows better leaf control.
*/
public boolean isLeaf() {
if (isNotLeaf) return false;
if (isLeaf) return true;
return (getChildCount() == 0);
}
/** Returns a unique name for this node.
*/
public String getName() {
return uname;
}
/** Returns a real name for this node (may not be unique).
*/
public String getXMLName() {
return name;
}
/** Sets the name for this node.
*/
public void setName(String newName) {
//check if name="..." is in args, else use name (and update uname)
XMLAttr attr;
boolean ok = false;
for(Iterator i = args.iterator(); i.hasNext();) {
attr = (XMLAttr)i.next();
if (attr.name.equals("name")) {attr.value = newName; ok = true; break;}
}
if (!ok) name = newName;
setuname(this);
}
/** Returns the child tag at index.
* This method just overrides the default method but returns XMLTag.
*/
public XMLTag getChildAt(int index) {
return (XMLTag)super.getChildAt(index);
}
/** Returns value of argument. */
public String getArg(String name) {
for(int a=0;a<args.size();a++) {
if (args.get(a).name.equals(name)) return args.get(a).value;
}
return null;
}
/** Returns the content of this node. */
public String getContent() {return content;}
};
/** The header tag.<br>
* <?xml version="1.0" encoding="UTF-8" ?>
*/
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(InputStream in) {
boolean quote = false, isArgs = false;
int ich;
char ch;
XMLTagPart tag = new XMLTagPart();
type = XML_DATA;
if (nexttype != -1) {type = nexttype; nexttype = -1;}
while (true) {
try {ich = in.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 == ' ') && (!isArgs)) isArgs = true;
if (isArgs) tag.args += 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 string2args(XMLTag tag, String args) {
//search for name="value"
char ca[] = args.toCharArray();
XMLAttr attr;
String name, value;
int length = args.length();
int ep;
tag.args.clear();
for(int a=0;a<length;a++) {
if (ca[a] == ' ') continue; //skip spaces
ep = args.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 = args.indexOf('\"', a);
if (ep == -1) return;
for(int b=a;b<ep;b++) value += ca[b];
a = ep+1;
} else {
ep = args.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.args.add(attr);
}
}
private void setuname(XMLTag tag) {
XMLTag parent = tag.getParent();
String uname = tag.name;
XMLAttr attr;
for(Iterator i = tag.args.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 = false;
try {
fis = new FileInputStream(filename);
ret = read(fis, event);
fis.close();
} catch (Exception 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 in, XMLEvent event) {
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(in);
if (tagpart == null) break;
switch (type) {
case XML_OPEN:
if (tagpart.content.startsWith("?xml")) {
if (bHeader) {JFLog.log("Multiple XML headers found"); return false;} //already read the XML header
header.name = tagpart.content;
header.uname = header.name;
string2args(header, tagpart.args);
if (event != null) event.XMLTagAdded(header);
break;
}
//no break
case XML_SINGLE:
if (tag == null) {
//root tag
if (bRoot) {JFLog.log("Multiple root tags found"); return false;} //already found a root tag
bRoot = true;
root.name = tagpart.content;
root.uname = root.name;
string2args(root, tagpart.args);
if (event != null) event.XMLTagAdded(root);
changedTag(root);
tag = root;
} else {
newtag = new XMLTag();
newtag.name = tagpart.content;
string2args(newtag, tagpart.args);
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 args2string(XMLTag tag) {
XMLAttr attr;
int size = tag.args.size();
String str = "", tmp;
for(int a=0;a<size;a++) {
attr = tag.args.get(a);
tmp = " " + attr.name + "=\"" + attr.value + "\"";
str += tmp;
}
return str;
}
private void writestr(OutputStream out, String str) {
// if (str.length() == 0) return;
try {out.write(str.getBytes());} 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 args;
if (size > 0) {
//write open tag w/ args + content
args = args2string(tag);
tmp = "<" + tag.name + args + ">\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 {
args = args2string(tag);
if (tag.isSingle) {
tmp = "<" + tag.name + args + "/>\n";
} else {
tmp = "<" + tag.name + args + ">" + 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 = false;
try {
fos = new FileOutputStream(filename);
ret = write(fos);
fos.close();
} catch (Exception e) {
return false;
}
return ret;
}
/** Writes the entire tree as a XML file to the OutputStream.
*/
public boolean write(OutputStream out) {
String tmp, args;
if (root.name.length() == 0) return false;
if (header.name.length() > 0) {
args = args2string(header);
tmp = "<" + header.name + args + ">\n";
writestr(out, tmp);
}
//write root header
args = args2string(root);
tmp = "<" + root.name + args + ">\n";
writestr(out, tmp);
int size = root.getChildCount();
indent = 2;
for(int a=0;a<size;a++) writetag(out, (XMLTag)root.getChildAt(a));
//write root tail
tmp = "</" + root.name + ">\n";
writestr(out, tmp);
return true;
}
private void clearTag(XMLTag tag) {
tag.name = "";
tag.args = new ArrayList<XMLAttr>();
tag.uname = "";
tag.content = "";
}
/** Deletes the entire tree and resets the root and header tags.
*/
public void deleteAll() {
deleteTag(root);
clearTag(header);
changedTag(header);
clearTag(root);
changedTag(root);
}
/** Deletes a node from the parent.
* Also deletes all it's children.
*/
public boolean deleteTag(XMLTag tag) {
//remove children first
while (tag.getChildCount() > 0) {
deleteTag((XMLTag)tag.getChildAt(0));
}
//now remove itself from parent
if (tag.getParent() == null) return true; //can not delete root/header tag itself
treemodel.removeNodeFromParent(tag);
return true;
}
private XMLEvent event = null;
/** Creates an empty node that can be inserted into the tree using addTag().
*/
public XMLTag createTag() {
return new XMLTag(); //must call addTag() to add to the tree
}
/** Adds the node to the targetParent.
*/
public XMLTag addTag(XMLTag targetParent, XMLTag tag) {
treemodel.insertNodeInto(tag, targetParent, targetParent.getChildCount());
setuname(tag);
if (event != null) event.XMLTagAdded(tag);
return tag;
}
/** Adds node with the name, args and content specified.
* If another node already exists with the same name the new node's unique name will differ.
*/
public XMLTag addTag(XMLTag targetParent, String name, String args, String content) {
XMLTag newtag = new XMLTag();
newtag.name = name;
newtag.content = content;
string2args(newtag, args);
return addTag(targetParent, newtag);
}
/** Adds (a non-existing) or sets (an existing) node with the name, args and content specified.
*/
public XMLTag addSetTag(XMLTag targetParent, String name, String args, String content) {
XMLTag child;
int len = targetParent.getChildCount();
for(int a=0;a<len;a++) {
child = targetParent.getChildAt(a);
if (child.name.equals(name)) {setTag(child, name, args, content); return child;}
}
return addTag(targetParent, name, args, content);
}
/** Notify the treemodel that you changed a node.
*/
public void changedTag(XMLTag tag) {
treemodel.nodeChanged(tag);
}
/** Sets the name, args and contents of the true root node.
*/
public void setRoot(String name, String args, String content) {
root.name = name;
string2args(root, args);
root.content = content;
changedTag(root);
}
/** Returns the unique name of a node.
*/
public String getName(XMLTag tag) {
return tag.getName();
}
/** Sets the name of a node.
* It's unique name may differ when shown in a tree.
*/
public void setName(XMLTag tag, String newName) {
tag.setName(newName);
}
/** Returns a node based on the TreePath path;
*/
public XMLTag getTag(TreePath path) {
return getTag(path.getPath());
}
/** Returns a node based on the objs[] path.
* Relative to virtual root tag. (see setRoot())
*/
public XMLTag getTag(Object objs[]) {
XMLTag tag = (XMLTag)treemodel.getRoot(), child;
String name;
if (objs == null || objs.length == 0) return null;
name = tag.getName();
if (!name.equals(objs[0].toString())) return null;
int idx = 1;
boolean ok;
int cnt;
while (idx < objs.length) {
ok = false;
cnt = tag.getChildCount();
for(int i=0;i<cnt;i++) {
child = (XMLTag)tag.getChildAt(i);
name = child.getName();
if (name.equals(objs[idx].toString())) {
ok = true;
idx++;
tag = child;
break;
}
}
if (!ok) return null; //next path element not found
}
return tag;
}
/** Sets the name, args and content for an existing node.
*/
public void setTag(XMLTag tag, String name, String args, String content) {
tag.name = name;
string2args(tag, args);
tag.content = content;
if (tag.getParent() != null) setuname(tag); else tag.uname = name;
if (event != null) event.XMLTagAdded(tag);
}
/** Sets the root node for the tree.
* This doesn't effect the true root node.
* This is usefull in hiding parts of a XML file from view when viewed in a JTree.
*/
public void setRoot(XMLTag newRoot) {
treemodel.setRoot(newRoot);
}
/** Writes all children of tag to a POJO Class.
* Fields not found in POJO are not saved.<br>
* Currently supports : int, boolean, String, Color.
* @return Number of elements saved.
*/
public int writeClass(XMLTag tag, Object pojo) {
Class<?> c = pojo.getClass();
Field f;
int cnt = tag.getChildCount();
int ret = 0;
XMLTag child;
String name, type;
for(int a=0;a<cnt;a++) {
try {
child = tag.getChildAt(a);
name = child.getName();
f = c.getField(name);
type = f.toGenericString();
if (type.indexOf(" int ") != -1) {
f.setInt(pojo, JF.atoi(child.content));
} else if (type.indexOf(" boolean ") != -1) {
f.setBoolean(pojo, child.content.equals("1") || child.content.equals("true"));
} else if (type.indexOf(" java.lang.String ") != -1) {
f.set(pojo, child.content);
} else if (type.indexOf(" java.awt.Color ") != -1) {
f.set(pojo, new Color(JF.atox(child.content)));
} else {
continue;
}
ret++;
} catch (Exception e) {}
}
return ret;
}
/** Reads all fields from a POJO Class to children of tag.
* Creates new children tags if needed.<br>
* Currently supports : int, boolean, String, Color.
* @return Number of elements loaded/created.
*/
public int readClass(XMLTag tag, Object pojo) {
Class<?> c = pojo.getClass();
Field fs[];
int ret = 0;
XMLTag child = null;
String name, type;
try {fs = c.getFields();} catch (Exception e) {return 0;}
int fieldcnt = fs.length;
int cnt;
boolean ok; //child tag already exists with field's name
for(int a=0;a<fieldcnt;a++) {
try {
ok = false;
cnt = tag.getChildCount();
name = fs[a].getName();
for(int b=0;b<cnt;b++) {
child = tag.getChildAt(b);
if (child.getName().equals(name)) {
ok = true;
break;
}
}
type = fs[a].toGenericString();
if (type.indexOf(" int ") != -1) {
if (ok) {
child.content = "" + fs[a].getInt(pojo);
} else {
addTag(tag, name, "", "" + fs[a].getInt(pojo));
}
} else if (type.indexOf(" boolean ") != -1) {
if (ok) {
child.content = fs[a].getBoolean(pojo) ? "true" : "false";
} else {
addTag(tag, name, "", fs[a].getBoolean(pojo) ? "true" : "false");
}
} else if (type.indexOf(" java.lang.String ") != -1) {
if (ok) {
child.content = (String)fs[a].get(pojo);
} else {
addTag(tag, name, "", (String)fs[a].get(pojo));
}
} else if (type.indexOf(" java.awt.Color ") != -1) {
if (ok) {
child.content = Integer.toHexString( ((Color)fs[a].get(pojo)).getRGB()).substring(2);
} else {
addTag(tag, name, "", Integer.toHexString( ((Color)fs[a].get(pojo)).getRGB()).substring(2));
}
} else {
continue;
}
ret++;
} catch (Exception e) {}
}
return ret;
}
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) {
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;
tag.setName(tag.getUserObject().toString());
if (event != null) event.XMLTagRenamed(tag);
}
public void treeNodesInserted(TreeModelEvent e) {
}
public void treeNodesRemoved(TreeModelEvent e) {
}
public void treeStructureChanged(TreeModelEvent e) {
}
};