/*
* Copyright 2011 Christian Thiemann <christian@spato.net>
* Developed at Northwestern University <http://rocs.northwestern.edu>
*
* This file is part of the SPaTo Visual Explorer (SPaTo).
*
* SPaTo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* SPaTo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with SPaTo. If not, see <http://www.gnu.org/licenses/>.
*/
package net.spato.sve.app;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Array;
import java.util.HashMap;
import java.util.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import net.spato.sve.app.layout.*;
import processing.core.PApplet;
import processing.xml.StdXMLBuilder;
import processing.xml.StdXMLParser;
import processing.xml.StdXMLReader;
import processing.xml.XMLElement;
import processing.xml.XMLValidator;
import processing.xml.XMLWriter;
import de.cthiemann.tGUI.*;
public class SPaToDocument {
protected static int untitledCounter = 0; // counter for uniquely numbering untitled documents
protected SPaTo_Visual_Explorer app = null;
XMLElement xmlDocument = new XMLElement("document");
SPaToView view = null;
File file = null; // corresponding file on disk
String name = null; // short name (basename of file or "<untitled%d>")
boolean compressed = false; // is this document in a zip archive or in a directory?
ZipFile zipfile = null;
ZipOutputStream zipout = null;
boolean modified = false;
HashMap<XMLElement,BinaryThing> blobCache = new HashMap<XMLElement,BinaryThing>();
public SPaToDocument(SPaTo_Visual_Explorer app) { this(app, null); }
public SPaToDocument(SPaTo_Visual_Explorer app, File file) {
this.app = app;
setFile(file);
this.view = new SPaToView(app, this);
}
/*
* Metadata Accessors
*/
public File getFile() { return file; }
public void setFile(File file) {
// assume it's a zip archive if it's not a directory; create zip files by default
setFile(file, (file == null) || !file.isDirectory());
}
public void setFile(File file, boolean compressed) {
try { this.file = file.getCanonicalFile(); } // try to normalize file path (remove "../" etc.)
catch (Exception e) { this.file = file; } // otherwise use original file
this.compressed = compressed;
this.name = null; // reset name
}
public boolean isCompressed() { return compressed; }
public void setCompressed(boolean compressed) { this.compressed = compressed; }
public void setCompressed() { setCompressed(true); }
public boolean isModified() { return modified; }
public String getName() {
if (name == null) {
if (file != null) {
name = file.getName(); // strip directory part
if (name.endsWith(".spato")) name = name.substring(0, name.length()-6); // strip .spato extension
} else
name = "<untitled" + (++untitledCounter) + ">";
}
return name;
}
public String getTitle() {
XMLElement xmlTitle = xmlDocument.getChild("title");
if (xmlTitle != null) {
String title = xmlTitle.getContent();
if (title != null)
return PApplet.trim(title);
}
return "Untitled Network";
}
public String getDescription() {
XMLElement xmlDescription = xmlDocument.getChild("description");
if (xmlDescription != null) {
String desc = xmlDescription.getContent();
if (desc != null)
return PApplet.join(PApplet.trim(PApplet.split(desc, '\n')), '\n');
}
return "";
}
/*
* Data Accessors
*/
public XMLElement getNodes() { return xmlDocument.getChild("nodes"); }
public XMLElement getNode(int i) { return getChild("nodes/node[" + i + "]"); }
public int getNodeCount() { return getChildren("nodes/node").length; }
public XMLElement getAlbum() { return xmlDocument == null ? null : xmlDocument.getChild("album"); } // FIXME: concurrency problems...
public XMLElement[] getAlbums() { return xmlDocument.getChildren("album"); }
public XMLElement getAlbum(String id) { return getChild("album[@id=" + id + "]"); }
public XMLElement getLinks() { return xmlDocument.getChild("links"); }
public XMLElement getSlices() { return xmlDocument.getChild("slices"); }
public XMLElement[] getDatasets() { return xmlDocument.getChildren("dataset"); }
public XMLElement getDataset(String id) { return getChild("dataset[@id=" + id + "]"); }
public XMLElement[] getAllQuantities() { return getChildren("dataset/data"); }
public XMLElement[] getQuantities() { return getChildren("dataset[@selected]/data"); }
public XMLElement[] getQuantities(XMLElement xmlDataset) { return xmlDataset.getChildren("data"); }
public XMLElement getQuantity(XMLElement xmlDataset, String id) {
return getChild(xmlDataset, "data[@id=" + id + "]"); }
public XMLElement[] getDistanceQuantities() { //return getChildren("dataset/data[@blobtype=float[N][N]]"); }
XMLElement res[] = new XMLElement[0];
for (XMLElement xmlData : getAllQuantities())
if (getSelectedSnapshot(xmlData).getString("blobtype", "").equals("float[N][N]"))
res = (XMLElement[])PApplet.append(res, xmlData);
return res;
}
public XMLElement getSelectedDataset() { return getChild("dataset[@selected]"); }
public XMLElement getSelectedQuantity() { return getChild("dataset[@selected]/data[@selected]"); }
public XMLElement getSelectedQuantity(XMLElement xmlDataset) {
return getChild(xmlDataset, "data[@selected]"); }
public void setSelectedDataset(XMLElement xmlDataset) {
XMLElement xmlOldDataset = getSelectedDataset();
if ((xmlDataset != null) && (xmlDataset == xmlOldDataset))
return; // nothing to do
if (xmlOldDataset != null) // unselect previously selected dataset
xmlOldDataset.remove("selected");
if (xmlDataset != null) { // select new dataset and make sure some quantity in it is selected
xmlDataset.setBoolean("selected", true);
if (getSelectedQuantity(xmlDataset) == null)
setSelectedQuantity(xmlDataset.getChild("data"));
}
view.setNodeColoringData(getSelectedQuantity());
}
public void setSelectedQuantity(XMLElement xmlData) {
XMLElement xmlOldData = getSelectedQuantity();
if ((xmlData != null) && (xmlData == xmlOldData))
return; // nothing to do
if ((xmlOldData != null) && ((xmlData == null) || (xmlOldData.getParent() == xmlData.getParent())))
xmlOldData.remove("selected"); // only unselect previous quantity if it's in the same dataset
if (xmlData != null) { // select new quantity and make sure the correct dataset is selected
xmlData.setBoolean("selected", true);
setSelectedDataset(xmlData.getParent());
}
view.setNodeColoringData(xmlData);
app.gui.updateNodeColoring();
}
public XMLElement getDistanceQuantity() { return getChild("dataset/data[@distmat]"); }
public void setDistanceQuantity(XMLElement xmlData) {
if ((xmlData != null) && (xmlData == view.xmlDistMat))
return; // nothing to do
if (view.xmlDistMat != null) view.xmlDistMat.remove("distmat");
view.setDistanceMatrix(xmlData);
if (xmlData != null) xmlData.setBoolean("distmat", true);
app.gui.updateProjection();
}
/*
* Snapshot Handling
*/
public XMLElement getSelectedSnapshot(XMLElement xml) { return getSelectedSnapshot(xml, true); }
public XMLElement getSelectedSnapshot(XMLElement xml, boolean recursive) {
if ((xml == null) || (xml.getChild("snapshot") == null))
return xml; // the snapshots are not strong in this one...
XMLElement result = null;
String album = xml.getChild("snapshot").getString("album");
if (album == null) { // this is a snapshot series, which means it's easy to find the selected snapshot
result = getChild(xml, "snapshot[@selected]");
if (result == null) { // looks like we're missing a 'selected' attribute...
result = xml.getChild("snapshot"); // ... so select the first one as default
if (result != null) xml.setBoolean("selected", true);
}
} else { // ... otherwise we have to do some more work
XMLElement xmlAlbum = getChild("album[@id=" + album + "]");
if (xmlAlbum == null) // this should not happen...
app.console.logError("Could not find album \u2018" + album + "\u2019, referenced in " + xml.getName() +
" \u201C" + xml.getString("name", xml.getString("id")) + "\u201D");
else {
XMLElement xmlSnapshot = getChild(xmlAlbum, "snapshot[@selected]");
if (xmlSnapshot == null) { // no snapshot is selected, try to select first one
xmlSnapshot = xmlAlbum.getChild("snapshot");
if (xmlSnapshot != null) xmlSnapshot.setBoolean("selected", true);
}
if (xmlSnapshot != null)
result = getChild(xml, "snapshot[@id=" + xmlSnapshot.getString("id") + "]");
}
}
return recursive ? getSelectedSnapshot(result) : result;
}
/** Returns the XML element containing the appropriate anonymous snapshot series (if any) of <code>xml</code>. */
public XMLElement getSelectedSnapshotSeriesContainer(XMLElement xml) {
while ((xml != null) && (xml.getChild("snapshot") != null) && (xml.getChild("snapshot").getString("album") != null))
xml = getSelectedSnapshot(xml, false);
return ((xml == null) || (xml.getChild("snapshot") == null)) ? null : xml;
}
/** Returns the index of the currently selected snapshot in an album or an anonymous snapshot series. */
public int getSelectedSnapshotIndex(XMLElement xml) {
if (xml == null) return -1;
if (!xml.getName().equals("album"))
xml = getSelectedSnapshotSeriesContainer(xml);
XMLElement snapshots[] = xml.getChildren("snapshot");
if (snapshots == null) return -1;
for (int i = 0; i < snapshots.length; i++)
if (snapshots[i].getBoolean("selected"))
return i;
if (snapshots.length > 0) { // no snapshot marked as selected?
snapshots[0].setBoolean("selected", true); // then mark the first one
return 0;
}
return -1;
}
public void setSelectedSnapshot(XMLElement xml, int index) { setSelectedSnapshot(xml, index, false); }
public void setSelectedSnapshot(XMLElement xml, int index, boolean relative) {
boolean isAlbum = xml.getName().equals("album");
if (!isAlbum)
xml = getSelectedSnapshotSeriesContainer(xml);
// find currently selected snapshot
XMLElement snapshots[] = xml.getChildren("snapshot");
if (snapshots == null) return; // should not happen
int selectedIndex = 0;
for (int i = snapshots.length - 1; i >= 0; i--) {
if (snapshots[i].getBoolean("selected")) selectedIndex = i;
snapshots[i].remove("selected");
}
// update currently selected snapshot
selectedIndex = relative ? selectedIndex + index : index;
while (selectedIndex < 0) selectedIndex += snapshots.length;
while (selectedIndex >= snapshots.length) selectedIndex -= snapshots.length;
snapshots[selectedIndex].setBoolean("selected", true);
// update view and GUI
if (isAlbum || (xml == getLinks())) { view.setLinks(getLinks()); }
if (isAlbum || (xml == getSlices())) {
view.setSlices(getSlices()); view.setTomLayout();
view.setDistanceMatrix(getDistanceQuantity());
/* FIXME: layout handling is bad... */ }
if (isAlbum || (xml == getSelectedQuantity())) { view.setNodeColoringData(getSelectedQuantity()); app.gui.updateNodeColoring(); }
if (isAlbum || (xml == getDistanceQuantity())) { view.setDistanceMatrix(getDistanceQuantity()); app.gui.updateProjection(); }
}
public XMLElement getColormap(XMLElement xml) {
xml = getSelectedSnapshot(xml);
while ((xml != null) && (xml.getChild("colormap") == null) && (xml.getName().equals("snapshot")))
xml = xml.getParent();
return (xml != null) ? xml.getChild("colormap") : null;
}
/*
* Binary Cache Accessors
*/
public BinaryThing getBlob(XMLElement xml) {
xml = getSelectedSnapshot(xml);
if (xml == null) return null;
BinaryThing blob = null;
if (!blobCache.containsKey(xml)) {
String xmlPretty = xml.getString("name", "");
if (xmlPretty.length() > 0) xmlPretty = " \u201C" + xmlPretty + "\u201D";
xmlPretty = xml.getName() + xmlPretty;
String name = xml.getString("blob");
InputStream stream = null;
try {
TConsole.Message msg = app.console.logProgress((name != null)
? "Loading " + xmlPretty + " from blob " + name
: "Parsing " + xmlPretty);
if (name != null) {
if ((stream = createDocPartInput("blobs" + File.separator + name)) != null)
blob = BinaryThing.loadFromStream(new DataInputStream(stream), msg);
} else
blob = BinaryThing.parseFromXML(xml, msg);
app.console.finishProgress();
} catch (Exception e) {
app.console.abortProgress("Error: ", e);
}
blobCache.put(xml, blob);
}
blob = blobCache.get(xml);
if (blob != null)
xml.setString("blobtype", getBlobType(blob));
return blob;
}
/* This is used to ensure a set of blobs is loaded. */
public void loadBlobs(XMLElement xml) { loadBlobs(new XMLElement[] { xml }); }
public void loadBlobs(XMLElement xml[]) {
if (xml == null) return;
for (int i = 0; i < xml.length; i++) {
XMLElement snapshots[] = xml[i].getChildren("snapshot");
if ((snapshots != null) && (snapshots.length > 0))
loadBlobs(snapshots); // load all snapshots (xml[i] holds no valid data)
else
getBlob(xml[i]); // getBlob will load/parse the data if not already cached
}
}
public void setBlob(XMLElement xml, Object blob) { setBlob(xml, blob, false); }
public void setBlob(XMLElement xml, Object blob, boolean persistent) {
if ((xml == null) || (blob == null)) return;
BinaryThing bt = new BinaryThing(blob);
blobCache.put(xml, bt);
xml.setString("blobtype", getBlobType(bt));
if (persistent) {
String blobname = xml.getString("id", generateID(xml.getString("label")));
XMLElement tmp = xml;
while ((tmp != null) && (tmp.getName().equals("snapshot")))
blobname = (tmp = tmp.getParent()).getString("id", generateID()) + "_" + blobname;
xml.setString("blob", blobname);
} else
xml.remove("blob");
}
/* This functions removes all stuff from the xml element that can be reproduced from the elements blob. */
// FIXME: this is not used at the moment; offer a choiceQuantity context menu item to call this
public void stripXMLData(XMLElement xml) {
XMLElement child = null;
if (xml.getName().equals("slices"))
while ((child = xml.getChild("slice")) != null)
xml.removeChild(child);
if (xml.getName().equals("data"))
while ((child = xml.getChild("values")) != null)
xml.removeChild(child);
// FIXME: handle links and snapshots
}
/* This returns a short description of the blob type/shape. */
public String getBlobType(BinaryThing blob) {
String s = blob.toString();
s = s.replaceAll("([^0-9])" + getNodeCount() + "([^0-9])", "$1N$2");
s = s.replaceAll("([^0-9])[23456789][0-9]*([^0-9])", "$1M$2"); // FIXME: replaces M for any number
return s;
}
/*
* Document Modification
*/
public void addDataset(XMLElement xmlDataset) {
xmlDocument.removeChild(xmlDataset); // avoid having it in there twice
if (xmlDataset.getString("id") == null)
xmlDataset.setString("id", generateID());
xmlDocument.addChild(xmlDataset);
app.gui.updateNodeColoring();
}
public void removeDataset(XMLElement xmlDataset) {
for (XMLElement xmlData : xmlDataset.getChildren("data"))
removeQuantity(xmlData);
if (xmlDataset.getBoolean("selected"))
setSelectedDataset((XMLElement)previousOrNext(getDatasets(), xmlDataset));
xmlDataset.getParent().removeChild(xmlDataset);
app.gui.updateNodeColoring();
}
public void addQuantity(XMLElement xmlDataset, XMLElement xmlData) { addQuantity(xmlDataset, xmlData, null); }
public void addQuantity(XMLElement xmlDataset, XMLElement xmlData, Object blob) {
xmlDataset.removeChild(xmlData); // make sure we won't add an already existing quantity
if (xmlData.getString("id") == null) xmlData.setString("id", generateID());
xmlDataset.addChild(xmlData);
if (blob != null) setBlob(xmlData, blob, true);
app.gui.updateNodeColoring();
app.gui.updateProjection();
}
public void removeQuantity(XMLElement xmlData) {
if (xmlData == view.xmlDistMat)
view.setDistanceMatrix((XMLElement)previousOrNext(getDistanceQuantities(), xmlData));
xmlData.getParent().removeChild(xmlData);
if (xmlData.getBoolean("selected"))
setSelectedQuantity((XMLElement)previousOrNext(getQuantities(), xmlData));
}
/*
* Loading/Saving Functions
*/
public Runnable newLoadingTask() {
return new Runnable() {
public void run() { loadFromDisk(); }
};
}
public Runnable newSavingTask() {
return new Runnable() {
public void run() { saveToDisk(); }
};
}
public void loadFromDisk(File file) { setFile(file); loadFromDisk(); }
public void loadFromDisk() {
TConsole.Message msg = app.console.logInfo("Loading from " + file.getAbsolutePath()).sticky();
// open zipfile or make sure the directory exists
try {
if (compressed) zipfile = new ZipFile(file);
else if (!file.exists()) throw new IOException("directory not found");
} catch (Exception e) {
app.console.logError("Error opening '" + file.getAbsolutePath() + "': ", e);
app.console.popSticky();
return;
}
// read XML document
if ((xmlDocument = addAutoIDs(readMultiPartXML("document.xml"))) != null) {
// setup nodes and map projection
XMLElement xmlNodes = getNodes();
if ((xmlNodes == null) || (getNodeCount() == 0))
app.console.logError("No nodes found");
else {
view.setNodes(xmlNodes);
view.setMapProjection(xmlNodes.getChild("projection"));
}
app.gui.updateAlbumControls();
// setup links
view.setLinks(getLinks());
if (getLinks() != null)
loadBlobs(getLinks()); // make sure all links snapshots are loaded
// setup data
view.setNodeColoringData(getSelectedQuantity());
loadBlobs(getAllQuantities()); // make sure all data is loaded
app.gui.updateNodeColoring();
// setup slices
view.setSlices(getSlices());
loadBlobs(getSlices()); // make sure all slices snapshots are loaded
generateLayouts(getSlices()); // FIXME
// setup tomogram layout
view.tomLayouts = xmlDocument.getString("tomLayouts", null); // FIXME: layouts should be specified by <layout> tags or something
view.setTomLayout();
view.setDistanceMatrix(getDistanceQuantity());
app.gui.updateProjection();
}
// clean-up and done
if (compressed) { try { zipfile.close(); } catch (Exception e) {}; zipfile = null; }
app.console.popSticky();
msg.text += " \u2013 done";
}
public void generateLayouts(XMLElement xml) { // FIXME
if (xml.getChild("snapshot") != null) // FIXME
for (XMLElement snapshot : xml.getChildren("snapshot")) // FIXME
generateLayouts(snapshot); // FIXME
else // FIXME
new Layout(app, getBlob(xml).getIntArray(), "radial_id"); // FIXME: the horror!
} // FIXME
public void saveToDisk() {
TConsole.Message msg = app.console.logInfo("Saving to " + file.getAbsolutePath()).sticky();
// open zipout or make sure the directory exists
try {
if (compressed) {
if (file.exists() && file.isDirectory())
if (!clearDirectory(file) || !file.delete()) // remove directory to be able to create regular file
throw new IOException("is a directory and could not be deleted");
zipout = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(file)));
zipout.setLevel(9);
} else {
if (file.exists() && !file.isDirectory())
if (!file.delete()) // remove regular file to be able to create directory
throw new IOException("file already exists and could not be deleted");
file.mkdirs();
if (!clearDirectory(file))
throw new IOException("could not clear the directory");
}
} catch (Exception e) {
app.console.logError("Error opening '" + file.getAbsolutePath() + "': ", e);
app.console.popSticky();
return;
}
// write XML document
writeMultiPartXML("document.xml", xmlDocument);
// write persistent blobs
Iterator<XMLElement> xmlit = blobCache.keySet().iterator();
while (xmlit.hasNext()) {
XMLElement xml = xmlit.next();
String name = xml.getString("blob");
if (name == null) continue; // not a persistant BinaryThing
if (!compressed) new File(file, "blobs").mkdirs();
OutputStream stream = createDocPartOutput("blobs" + File.separator + name);
if (stream != null) try {
String xmlPretty = xml.getString("name", "");
if (xmlPretty.length() > 0) xmlPretty = " \u201C" + xmlPretty + "\u201D";
xmlPretty = xml.getName() + xmlPretty;
TConsole.Message msgBlob = app.console.logProgress("Saving " + xmlPretty + " to blob " + name);
blobCache.get(xml).saveToStream(new DataOutputStream(stream), msgBlob);
app.console.finishProgress();
if (!compressed) stream.close();
} catch (Exception e) { app.console.abortProgress("Error saving blob " + name + ": ", e); }
}
// clean-up and done
if (compressed) { try { zipout.close(); } catch (Exception e) {}; zipout = null; }
app.console.popSticky();
msg.text += " \u2013 done";
}
public InputStream createDocPartInput(String name) {
InputStream stream = null;
if (compressed) {
name = name.replace(File.separatorChar, '/'); // always use / as file separator in zip files
try { stream = zipfile.getInputStream(zipfile.getEntry(name)); }
catch (Exception e) { stream = null; }
} else
stream = app.createInput(new File(file, name).getAbsolutePath());
if (stream == null)
app.console.logError("Could not find or read '" + name + "' in '" + file.getAbsolutePath() + "'");
return new BufferedInputStream(stream);
}
public OutputStream createDocPartOutput(String name) {
OutputStream stream = null;
if (compressed) {
name = name.replace(File.separatorChar, '/'); // always use / as file separator in zip files
try { zipout.putNextEntry(new ZipEntry(name)); stream = zipout; }
catch (Exception e) { stream = null; }
} else
stream = app.createOutput(new File(file, name).getAbsolutePath());
if (stream == null)
app.console.logError("Error opening '" + name + "' in '" + file.getAbsolutePath() + "' for writing");
return compressed ? stream : new BufferedOutputStream(stream); // zipout is already buffered
}
public Reader createDocPartReader(String name) {
try { return new InputStreamReader(createDocPartInput(name)); } catch (Exception e) { return null; } }
public Writer createDocPartWriter(String name) {
try { return new OutputStreamWriter(createDocPartOutput(name)); } catch (Exception e) { return null; } }
/* Copy of processing.xml.XMLElement.parseFromReader(...), modified to fit our needs (i.e., catch parsing
* exceptions and return null instead of silently ignoring and returning a crippled document). */
public XMLElement readXML(String name) {
Reader reader = new File(name).isAbsolute() ? app.createReader(name) : createDocPartReader(name);
if (reader == null) return null;
XMLElement xml = new XMLElement();
try {
app.console.logProgress("Parsing " + name).indeterminate();
StdXMLParser parser = new StdXMLParser();
parser.setBuilder(new StdXMLBuilder(xml));
parser.setValidator(new XMLValidator());
parser.setReader(new StdXMLReader(reader));
parser.parse();
app.console.finishProgress();
} catch (Exception e) {
app.console.abortProgress("XML parsing error in " + name + ": ", e);
xml = null;
}
try { reader.close(); } catch (Exception e) { }
return xml;
}
public void writeXML(String name, XMLElement xml) {
Writer writer = new File(name).isAbsolute() ? app.createWriter(name) : createDocPartWriter(name);
if (writer == null) return;
try {
app.console.logProgress("Writing " + name).indeterminate();
writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
new XMLWriter(writer).write(xml, true);
if (!compressed) writer.close(); // we're directly writing into the zipfile, which will be closed by saveToDisk
app.console.finishProgress();
} catch (Exception e) {
app.console.abortProgress("XML writing error in " + name + ": ", e);
}
}
/* This function reads the XML document found in file specified by name. It then traverses
* all child tags and wherever it finds a "src" attribute it will interpret its value as
* a URI (absolute or relative to the document base) of an additional XML file. That file is
* parsed and searched for a tag (either top-level or child of top-level) that matches the
* tag in the original document in name and (if existent) "id" attribute value. If a match is
* found, the tag from the external document is merged into the tag in the original document,
* that is, tag attributes are added and the tag's children are appended to the original tag's
* children. The returned XMLElement is the merged full document.
*/
public XMLElement readMultiPartXML(String name) {
XMLElement doc = readXML(name);
if (doc == null) return null;
for (int i = 0; i < doc.getChildCount(); i++) {
XMLElement child = doc.getChild(i);
// load external XML document
String src = child.getString("src");
if (src == null) continue;
XMLElement idoc = readXML(src);
if (idoc == null) continue;
// find the matching node to merge into the original document
XMLElement merge = equalNameAndID(child, idoc, true) ? idoc : null;
if (merge == null)
for (int j = 0; j < idoc.getChildCount(); j++)
if (equalNameAndID(child, idoc.getChild(j), false))
merge = idoc.getChild(j);
if (merge == null) {
app.console.logError(child.getName() + ": no match found in " + src);
continue;
}
// merge node with 'child'
String[] attr = merge.listAttributes();
for (int j = 0; j < attr.length; j++)
if (!attr[j].equals("src"))
child.setString(attr[j], merge.getString(attr[j]));
XMLElement[] children = merge.getChildren();
for (int j = 0; j < children.length; j++)
child.addChild(children[j]);
}
return doc;
}
/* Being the counterpart to readMultiPartXML, this function traverses the children of the document doc
* to extract all parts which should go into a separate file. The first child with a "src" attribute
* that is not already recorded in the map will be replaced by a new element that has the same tag name,
* same "id" attribute (if applicable), and the same "src" attribute (as a hint for subsequent reading from disk).
* The "src" value and the extracted element are added to the hash map and the function calls itself recursively.
* Afterwards, the removed element is re-added into the document. If no such children are found,
* the remaining document is written to the file specified by name, and all extracted children to their
* respective files. */
public void writeMultiPartXML(String name, XMLElement doc) { writeMultiPartXML(name, doc, null); }
public void writeMultiPartXML(String name, XMLElement doc, HashMap<String,XMLElement[]> map) {
if (doc == null) return;
if (map == null) map = new HashMap<String,XMLElement[]>();
int iChild = -1; XMLElement child = null; String src = null;
for (int i = 0; i < doc.getChildCount(); i++) {
child = doc.getChild(i);
src = child.getString("src");
if ((src != null) && notInMap(map, src, child)) { iChild = i; break; }
}
if (iChild == -1) {
// write doc to name
writeXML(name, doc);
// write all extracted children to their respective files
Object srcs[] = map.keySet().toArray();
for (int i = 0; i < srcs.length; i++) {
XMLElement[] elems = map.get(srcs[i]);
XMLElement out = new XMLElement("includes");
for (int j = 0; j < elems.length; j++) elems[j].remove("src");
if (elems.length == 1)
out = elems[0]; // no need to wrap into another element if we write only one anyway
else for (int j = 0; j < elems.length; j++)
out.addChild(elems[j]);
writeXML((String)srcs[i], out);
for (int j = 0; j < elems.length; j++) elems[j].setString("src", (String)srcs[i]);
}
} else {
// add child to map
if (!map.containsKey(src)) map.put(src, new XMLElement[0]);
map.put(src, (XMLElement[])PApplet.append(map.get(src), child));
// extract from doc
XMLElement childPlaceholder = new XMLElement(child.getName());
if (child.getString("id") != null)
childPlaceholder.setString("id", child.getString("id"));
childPlaceholder.setString("src", src);
doc.insertChild(childPlaceholder, iChild);
doc.removeChild(child);
// recurse on doc
writeMultiPartXML(name, doc, map);
// re-insert child
doc.insertChild(child, iChild);
doc.removeChild(childPlaceholder);
}
}
public boolean notInMap(HashMap<String,XMLElement[]> map, String src, XMLElement child) {
if (!map.containsKey(src)) return true;
XMLElement elems[] = map.get(src);
for (int i = 0; i < elems.length; i++) if (equalNameAndID(elems[i], child, false)) return false;
return true;
}
/* This function traverses the XML document and adds auto-generated id attribute values
* to all links, slices, dataset, and data tags, if they are missing their id attribute.
* Note that this function will alter its argument doc (and return a reference to it). */
public XMLElement addAutoIDs(XMLElement doc) { return addAutoIDs(doc, ""); }
public XMLElement addAutoIDs(XMLElement doc, String hashPrefix) {
if (doc == null) return null;
String tags[] = { "links", "slices", "dataset", "data" };
for (int t = 0; t < tags.length; t++) {
XMLElement elems[] = doc.getChildren(tags[t]);
for (int i = 0; i < elems.length; i++) {
if (elems[i].getString("id") == null)
elems[i].setString("id", generateID(hashPrefix + elems[i].getString("name", "")));
if (tags[t].equals("dataset"))
addAutoIDs(elems[i], elems[i].getString("id"));
}
}
return doc;
}
/*
* Small Helper Functions
*/
/* Returns true if the names of a and b match (or are both null) and if the "id" attributes
* of a and b match. If laxID is true, the test still passes if a has an id attribute but
* b does not. */
public boolean equalNameAndID(XMLElement a, XMLElement b, boolean laxID) {
return (((b.getName() == null) && (a.getName() == null)) ||
((b.getName() != null) && b.getName().equals(a.getName()))) &&
(((b.getString("id") == null) && (laxID || (a.getString("id") == null))) ||
((b.getString("id") != null) && b.getString("id").equals(a.getString("id"))));
}
/* This function will always return a most-probably unique 8-digit hex-character sequence.
* It does so by returning the first 8 characters of the MD5 hash of the argument name,
* or a random sequence if name is null, an empty string, or something goes wrong with MD5. */
public String generateID() { return generateID(null); }
public String generateID(String name) {
byte[] digest = new byte[4];
// generate random 8-byte sequence as a backup
for (int i = 0; i < 4; i++)
digest[i] = (byte)PApplet.parseInt(app.random(256));
// try calculating MD5 hash if name argument is sane
if ((name != null) && !name.equals(""))
try { digest = java.security.MessageDigest.getInstance("MD5").digest(name.getBytes()); } catch (Exception e) { }
// serialize and return whatever we got
String id = "";
for (int i = 0; i < 4; i++)
id += String.format("%02x", digest[i] & 0xff);
return id;
}
/* Recursively removes the contents of the specified directory. */
public boolean clearDirectory(File f) throws Exception {
if (!f.isDirectory()) return false;
File ff[] = f.listFiles();
for (int i = 0; i < ff.length; i++) {
if (ff[i].isDirectory() && !clearDirectory(ff[i])) return false;
if (!ff[i].delete()) return false;
}
return true;
}
/* If needle is in the array haystack, then the element before needle is returned.
* If needle is the first element in haystack, the element after needle is returned.
* If haystack only contains needle, null is returned. */
public Object previousOrNext(Object haystack, Object needle) {
if ((Array.getLength(haystack) > 1) && (Array.get(haystack, 0) == needle))
return Array.get(haystack, 1); // return second element if needle is first
for (int i = 1; i < Array.getLength(haystack); i++)
if (Array.get(haystack, i) == needle)
return Array.get(haystack, i - 1); // return element before needle
return null; // needle was not in haystack
}
/* Evaluates an XPath expression on xmlDocument. Only understands a limited subet of XPath! */
public XMLElement[] getChildren(String path) { return getChildren(xmlDocument, path); }
public XMLElement[] getChildren(XMLElement xml, String path) {
XMLElement result[] = new XMLElement[0];
if ((xml == null) || (path == null))
return result;
String name = path, conds = "", subpath = null; int p;
if ((p = path.indexOf('/')) > -1) {
name = path.substring(0, p);
subpath = path.substring(p+1);
}
if ((p = name.indexOf('[')) > -1) {
conds = name.substring(p);
name = name.substring(0, p);
}
XMLElement tmp[] = name.equals("*") ? xml.getChildren() : xml.getChildren(name);
int level = 0, i0 = 0;
for (int i = 0; i < conds.length(); i++) {
if (conds.charAt(i) == '[')
if (level++ == 0) i0 = i;
if (conds.charAt(i) == ']')
if (--level == 0)
tmp = xpathFilter(tmp, conds.substring(i0 + 1, i));
}
if (subpath != null) {
for (int i = 0; i < tmp.length; i++)
result = (XMLElement[])PApplet.concat(result, getChildren(tmp[i], subpath));
} else
result = tmp;
return result;
}
public XMLElement getChild(String path) { return getChild(xmlDocument, path); }
public XMLElement getChild(XMLElement xml, String path) {
XMLElement result[] = getChildren(xml, path);
return ((result != null) && (result.length > 0)) ? result[0] : null;
}
public XMLElement[] xpathFilter(XMLElement xml[], String condition) {
if (xml == null)
return new XMLElement[0];
if ((xml.length == 0) || (condition == null) || (condition.length() == 0))
return xml;
if (condition.charAt(0) == '@') {
int p = condition.indexOf('=');
String attr = (p > -1) ? condition.substring(1, p) : condition.substring(1);
String value = (p > -1) ? condition.substring(p+1) : null; // FIXME: '/" unwrapping
XMLElement result[] = new XMLElement[0];
for (int i = 0; i < xml.length; i++) {
if (xml[i].getString(attr) == null) continue;
if ((value != null) && !value.equals(xml[i].getString(attr))) continue;
result = (XMLElement[])PApplet.append(result, xml[i]);
}
return result;
} else
return xml;
}
public String toString() { return super.toString() + "[" + getName() + "]"; }
}