package nodebox.node;
import com.google.common.base.Objects;
import nodebox.function.CoreFunctions;
import nodebox.function.FunctionLibrary;
import nodebox.function.FunctionRepository;
import nodebox.graphics.Point;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.StringWriter;
import java.io.Writer;
import java.util.*;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Writes the ndbx file format.
*/
public class NDBXWriter {
public static void write(NodeLibrary library, File file) {
StreamResult streamResult = new StreamResult(file);
write(library, streamResult, file);
}
public static void write(NodeLibrary library, Writer writer) {
StreamResult streamResult = new StreamResult(writer);
write(library, streamResult, null);
}
public static void write(NodeLibrary library, StreamResult streamResult, File file) {
try {
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = builder.newDocument();
// Build the header.
Element rootElement = doc.createElement("ndbx");
rootElement.setAttribute("type", "file");
rootElement.setAttribute("formatVersion", NodeLibrary.CURRENT_FORMAT_VERSION);
rootElement.setAttribute("uuid", library.getUuid().toString());
doc.appendChild(rootElement);
// Write out all the document properties.
Set<String> propertyNames = library.getPropertyNames();
ArrayList<String> orderedNames = new ArrayList<String>(propertyNames);
Collections.sort(orderedNames);
for (String propertyName : orderedNames) {
String propertyValue = library.getProperty(propertyName);
Element e = doc.createElement("property");
e.setAttribute("name", propertyName);
e.setAttribute("value", propertyValue);
rootElement.appendChild(e);
}
// Write the function repository.
writeFunctionRepository(doc, rootElement, library.getFunctionRepository(), file);
writeDevices(doc, rootElement, library.getDevices());
// Write the root node.
writeNode(doc, rootElement, library.getRoot(), library.getNodeRepository());
// Convert the document to XML.
DOMSource domSource = new DOMSource(doc);
TransformerFactory tf = TransformerFactory.newInstance();
Transformer serializer = tf.newTransformer();
serializer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
serializer.setOutputProperty(OutputKeys.INDENT, "yes");
serializer.transform(domSource, streamResult);
} catch (ParserConfigurationException e) {
throw new RuntimeException(e);
} catch (TransformerException e) {
throw new RuntimeException(e);
}
}
public static String asString(NodeLibrary library) {
StringWriter writer = new StringWriter();
write(library, writer);
return writer.toString();
}
/**
* Write out links to the function repositories used.
*
* @param doc the XML document
* @param parent the parent element
* @param functionRepository the function repository to write
* @param baseFile the file to which the paths of the function libraries are relative to.
*/
private static void writeFunctionRepository(Document doc, Element parent, FunctionRepository functionRepository, File baseFile) {
for (FunctionLibrary library : functionRepository.getLibraries()) {
// The core functions library is implicitly included.
if (library == CoreFunctions.LIBRARY) continue;
Element el = doc.createElement("link");
el.setAttribute("rel", "functions");
el.setAttribute("href", library.getLink(baseFile));
parent.appendChild(el);
}
}
/**
* Write out external devices.
*
* @param doc the XML document
* @param parent the parent element
* @param devices the external devices to write
*/
private static void writeDevices(Document doc, Element parent, List<Device> devices) {
for (Device device : devices) {
Element el = doc.createElement("device");
el.setAttribute("name", device.getName());
el.setAttribute("type", device.getType());
for (Map.Entry<String, String> property : device.getProperties().entrySet()) {
Element e = doc.createElement("property");
e.setAttribute("name", property.getKey());
e.setAttribute("value", property.getValue());
el.appendChild(e);
}
parent.appendChild(el);
}
}
/**
* Find the libraryname.nodename of the given node.
* Searches the list of default node repositories to find it.
*
* @param node The node to find.
* @param nodeRepository The list of node libraries to look for the node.
* @return the node id, in the format libraryname.nodename.
*/
private static String findNodeId(Node node, NodeRepository nodeRepository) {
NodeLibrary library = nodeRepository.nodeLibraryForNode(node);
if (library == null) {
return node.getName();
} else {
return String.format("%s.%s", library.getName(), node.getName());
}
}
/**
* Write out the node.
*
* @param doc the XML document
* @param parent the parent element
* @param node the node to write
* @param nodeRepository the repository that contains the node prototype
*/
private static void writeNode(Document doc, Element parent, Node node, NodeRepository nodeRepository) {
Element el = doc.createElement("node");
parent.appendChild(el);
// Write prototype
if (shouldWriteAttribute(node, Node.Attribute.PROTOTYPE)) {
if (node.getPrototype() != Node.ROOT)
el.setAttribute("prototype", findNodeId(node.getPrototype(), nodeRepository));
}
// Write name
if (shouldWriteAttribute(node, Node.Attribute.NAME))
el.setAttribute("name", node.getName());
// Write comment
if (shouldWriteAttribute(node, Node.Attribute.COMMENT))
el.setAttribute("comment", node.getComment());
// Write category
if (shouldWriteAttribute(node, Node.Attribute.CATEGORY))
el.setAttribute("category", node.getCategory());
// Write description
if (shouldWriteAttribute(node, Node.Attribute.DESCRIPTION))
el.setAttribute("description", node.getDescription());
// Write output type
if (shouldWriteAttribute(node, Node.Attribute.OUTPUT_TYPE))
el.setAttribute("outputType", node.getOutputType());
// Write output range
if (shouldWriteAttribute(node, Node.Attribute.OUTPUT_RANGE))
el.setAttribute("outputRange", node.getOutputRange().toString().toLowerCase());
// Write image
if (shouldWriteAttribute(node, Node.Attribute.IMAGE))
el.setAttribute("image", node.getImage());
// Write function
if (shouldWriteAttribute(node, Node.Attribute.FUNCTION))
el.setAttribute("function", node.getFunction());
// Write handle function
if (shouldWriteAttribute(node, Node.Attribute.HANDLE))
el.setAttribute("handle", node.getHandle());
// Write position
if (shouldWriteAttribute(node, Node.Attribute.POSITION)) {
Point position = node.getPosition();
el.setAttribute("position", String.valueOf(position));
}
// Write rendered child
if (shouldWriteAttribute(node, Node.Attribute.RENDERED_CHILD_NAME))
el.setAttribute("renderedChild", node.getRenderedChildName());
// Add the children
if (shouldWriteAttribute(node, Node.Attribute.CHILDREN)) {
// Sort the children.
ArrayList<Node> children = new ArrayList<Node>();
children.addAll(node.getChildren());
Collections.sort(children, new NodeNameComparator());
// The order in which the nodes are written is important!
// Since a library can potentially store an instance and its prototype, make sure that the prototype gets
// stored sequentially before its instance.
// The reader expects prototypes to be defined before their instances.
while (!children.isEmpty()) {
Node child = children.get(0);
writeOrderedChild(doc, el, children, child, nodeRepository);
}
}
// Add the input ports
if (shouldWriteAttribute(node, Node.Attribute.INPUTS)) {
for (Port port : node.getInputs()) {
writePort(doc, el, node, port, Port.Direction.INPUT);
}
}
// Add all child connections
if (shouldWriteAttribute(node, Node.Attribute.CONNECTIONS)) {
for (Connection conn : node.getConnections()) {
writeConnection(doc, el, conn);
}
}
}
/**
* Check if the given attribute should be written.
* <p/>
* The attribute should be written if it's value is different from the prototype value.
*
* @param node The node.
* @param attribute The name of the attribute.
* @return true if the attribute should be written.
*/
private static boolean shouldWriteAttribute(Node node, Node.Attribute attribute) {
checkArgument(node != Node.ROOT, "You cannot write out the ROOT node.");
Object prototypeValue = node.getPrototype().getAttributeValue(attribute);
Object nodeValue = node.getAttributeValue(attribute);
if (attribute != Node.Attribute.PROTOTYPE) {
checkNotNull(prototypeValue, "Attribute %s of node %s is empty.", attribute, node.getPrototype());
checkNotNull(nodeValue, "Attribute %s of node %s is empty.", attribute, node);
return !prototypeValue.equals(nodeValue);
} else {
return prototypeValue != nodeValue;
}
}
/**
* Write out the child. If the prototype of the child is also in this library, write that out first, recursively.
*
* @param doc the XML document
* @param parent the parent element
* @param children a list of children that were written already.
* When a child is written, we remove it from the list.
* @param child the child to write
* @param nodeRepository the node repository that contains the node prototype
*/
private static void writeOrderedChild(Document doc, Element parent, List<Node> children, Node child, NodeRepository nodeRepository) {
Node prototype = child.getPrototype();
if (children.contains(prototype))
writeOrderedChild(doc, parent, children, prototype, nodeRepository);
writeNode(doc, parent, child, nodeRepository);
children.remove(child);
}
/**
* Check if the given attribute should be written.
* <p/>
* The attribute should be written if it's value is different from the prototype value.
*
* @param node The node.
* @param port The port.
* @param attribute The name of the attribute.
* @return true if the attribute should be written.
*/
private static boolean shouldWriteAttribute(Node node, Port port, Port.Attribute attribute) {
checkArgument(node != Node.ROOT, "You cannot write out the ROOT node.");
Port prototypePort = node.getPrototype().getInput(port.getName());
// If there is no prototype port, we should always write the attribute.
if (prototypePort == null) return true;
Object prototypeValue = prototypePort.getAttributeValue(attribute);
Object value = port.getAttributeValue(attribute);
// Objects.equal does the correct null-comparison for min / max values.
return !Objects.equal(prototypeValue, value);
}
private static void writePort(Document doc, Element parent, Node node, Port port, Port.Direction direction) {
// We only write out the ports that have changed with regards to the prototype.
Node protoNode = node.getPrototype();
Port protoPort = null;
if (protoNode != null)
protoPort = protoNode.getInput(port.getName());
// If the port and its prototype are equal, don't write anything.
if (port.equals(protoPort)) return;
Element el = doc.createElement("port");
el.setAttribute("name", port.getName());
el.setAttribute("type", port.getType());
if (shouldWriteAttribute(node, port, Port.Attribute.LABEL))
el.setAttribute("label", port.getLabel());
if (shouldWriteAttribute(node, port, Port.Attribute.CHILD_REFERENCE) && port.getChildReference() != null)
el.setAttribute("childReference", port.getChildReference());
if (shouldWriteAttribute(node, port, Port.Attribute.WIDGET))
el.setAttribute("widget", port.getWidget().toString().toLowerCase());
if (shouldWriteAttribute(node, port, Port.Attribute.RANGE))
el.setAttribute("range", port.getRange().toString().toLowerCase());
if (port.isStandardType())
el.setAttribute("value", port.stringValue());
if (shouldWriteAttribute(node, port, Port.Attribute.DESCRIPTION))
el.setAttribute("description", port.getDescription());
if (shouldWriteAttribute(node, port, Port.Attribute.MINIMUM_VALUE))
if (port.getMinimumValue() != null)
el.setAttribute("min", String.format(Locale.US, "%s", port.getMinimumValue()));
if (shouldWriteAttribute(node, port, Port.Attribute.MAXIMUM_VALUE))
if (port.getMaximumValue() != null)
el.setAttribute("max", String.format(Locale.US, "%s", port.getMaximumValue()));
if (shouldWriteAttribute(node, port, Port.Attribute.MENU_ITEMS))
writeMenuItems(doc, el, port.getMenuItems());
parent.appendChild(el);
}
private static void writeMenuItems(Document doc, Element parent, List<MenuItem> menuItems) {
for (MenuItem item : menuItems) {
Element el = doc.createElement("menu");
el.setAttribute("key", item.getKey());
el.setAttribute("label", item.getLabel());
parent.appendChild(el);
}
}
private static void writeConnection(Document doc, Element parent, Connection conn) {
Element connElement = doc.createElement("conn");
connElement.setAttribute("output", String.format("%s", conn.getOutputNode()));
connElement.setAttribute("input", String.format("%s.%s", conn.getInputNode(), conn.getInputPort()));
parent.appendChild(connElement);
}
private static class NodeNameComparator implements Comparator<Node> {
public int compare(Node node1, Node node2) {
return node1.getName().compareTo(node2.getName());
}
}
}