package nodebox.node;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
* Parses the ndbx file format.
*
* Because you can define both prototypes and "instances" that use these prototypes in the same file,
* you have to make sure that you define the prototypes before you instantiate them. NDBX can't handle
* files with arbitrary ordering. NodeLibrary.store() has support for sequential ordering of prototypes.
* This does not concern prototypes that are defined in other files or are builtin, since they will be
* loaded already.
*/
public class NDBXHandler extends DefaultHandler {
enum ParseState {
INVALID, IN_CODE, IN_DESCRIPTION, IN_VALUE, IN_MENU, IN_EXPRESSION
}
enum CodeType {
INVALID, PYTHON, JAVA
}
public static final String NDBX_FORMAT_VERSION = "formatVersion";
public static final String VAR_NAME = "name";
public static final String VAR_VALUE = "value";
public static final String CODE_TYPE = "type";
public static final String NODE_NAME = "name";
public static final String NODE_PROTOTYPE = "prototype";
public static final String NODE_TYPE = "type";
public static final String NODE_X = "x";
public static final String NODE_Y = "y";
public static final String NODE_RENDERED = "rendered";
public static final String NODE_EXPORTED = "exported";
public static final String MENU_KEY = "key";
public static final String PARAMETER_NAME = "name";
public static final String PARAMETER_TYPE = "type";
public static final String PARAMETER_WIDGET = "widget";
public static final String PARAMETER_LABEL = "label";
public static final String PARAMETER_HELP_TEXT = "help";
public static final String PARAMETER_DISPLAY_LEVEL = "display";
public static final String PARAMETER_ENABLE_EXPRESSION = "enableExpression";
public static final String PARAMETER_BOUNDING_METHOD = "bounding";
public static final String PARAMETER_MINIMUM_VALUE = "min";
public static final String PARAMETER_MAXIMUM_VALUE = "max";
public static final String VALUE_TYPE = "type";
public static final String PORT_NAME = "name";
public static final String PORT_CARDINALITY = "cardinality";
public static final String CONNECTION_OUTPUT = "output";
public static final String CONNECTION_INPUT = "input";
public static final String CONNECTION_PORT = "port";
private NodeLibraryManager manager;
private NodeLibrary library;
private Node rootNode;
private Node currentNode;
private Parameter currentParameter;
private String currentMenuKey;
private CodeType currentCodeType = CodeType.INVALID;
private Map expressionMap = new HashMap();
private ParseState state = ParseState.INVALID;
private StringBuffer characterData;
public NDBXHandler(NodeLibrary library, NodeLibraryManager manager) {
this.manager = manager;
this.library = library;
this.rootNode = library.getRootNode();
currentNode = null;
}
public NodeLibrary geNodeLibrary() {
return library;
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if (qName.equals("ndbx")) {
startNdbxTag(attributes);
} else if (qName.equals("var")) {
startVarTag(attributes);
} else if (qName.equals("code")) {
startCodeTag(attributes);
} else if (qName.equals("node")) {
startNodeTag(attributes);
} else if (qName.equals("description")) {
startDescriptionTag(attributes);
} else if (qName.equals("param")) {
startParameterTag(attributes);
} else if (qName.equals("value")) {
startValueTag(attributes);
} else if (qName.equals("expression")) {
startExpressionTag(attributes);
} else if (qName.equals("menu")) {
startMenuTag(attributes);
} else if (qName.equals("port")) {
startPortTag(attributes);
} else if (qName.equals("conn")) {
startConnectionTag(attributes);
} else {
throw new SAXException("Unknown tag " + qName);
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if (qName.equals("ndbx")) {
// Top level element -- parsing finished.
} else if (qName.equals("var")) {
// Do nothing after var tag
} else if (qName.equals("code")) {
setLibraryCode(characterData.toString());
resetState();
currentCodeType = CodeType.INVALID;
} else if (qName.equals("node")) {
// Traverse up to the parent.
// This can result in currentNode being null if we traversed all the way up
currentNode = currentNode.getParent();
} else if (qName.equals("description")) {
setDescription(characterData.toString());
resetState();
} else if (qName.equals("param")) {
currentParameter = null;
} else if (qName.equals("value")) {
setValue(characterData.toString());
resetState();
currentCodeType = CodeType.INVALID;
} else if (qName.equals("expression")) {
setTemporaryExpression(characterData.toString());
resetState();
} else if (qName.equals("menu")) {
setMenuItem(characterData.toString());
resetState();
currentMenuKey = null;
} else if (qName.equals("port")) {
// Do nothing after port tag
} else if (qName.equals("conn")) {
// Do nothing after conn tag
} else {
// This should never happen, since the SAX parser has already formally validated the document.
// Unknown tags should be caught in startElement.
throw new AssertionError("Unknown end tag " + qName);
}
}
/**
* Called after valid character data was processed.
*
* This makes sure no extraneous data is added.
*/
private void resetState() {
state = ParseState.INVALID;
characterData = null;
currentMenuKey = null;
}
@Override
public void endDocument() throws SAXException {
// Since parameter expressions can refer to arbitrary other parameters in the network,
// we need to have the fully created document first before setting expressions.
// Expressions are evaluated once they are set so they can create dependencies on other
// parameters.
for (Map.Entry entry : expressionMap.entrySet()) {
entry.getKey().setExpression(entry.getValue());
}
}
private void startNdbxTag(Attributes attributes) throws SAXException {
// Make sure we use the correct format and file type.
String formatVersion = attributes.getValue(NDBX_FORMAT_VERSION);
if (formatVersion == null)
throw new SAXException("NodeBox file does not have required attribute formatVersion.");
if (!formatVersion.equals("0.9"))
throw new SAXException("Unknown formatVersion " + formatVersion);
}
private void startVarTag(Attributes attributes) throws SAXException {
// Variables that get stored in the NodeBox library.
String name = attributes.getValue(VAR_NAME);
String value = attributes.getValue(VAR_VALUE);
if (name == null) throw new SAXException("Name attribute is required in var tags.");
if (value == null) throw new SAXException("Value attribute is required in var tags.");
library.setVariable(name, value);
}
private void startCodeTag(Attributes attributes) throws SAXException {
String type = attributes.getValue(CODE_TYPE);
if (type == null) throw new SAXException("Type attribute is required in code tags.");
try {
currentCodeType = CodeType.valueOf(type.toUpperCase(Locale.US));
} catch (IllegalArgumentException e) {
throw new SAXException("Invalid type attribute in code tag: should be python or java, not " + type + ".");
}
state = ParseState.IN_CODE;
characterData = new StringBuffer();
}
private void setLibraryCode(String code) throws SAXException {
library.setCode(parseCode(code));
}
private void startNodeTag(Attributes attributes) throws SAXException {
String name = attributes.getValue(NODE_NAME);
String prototypeId = attributes.getValue(NODE_PROTOTYPE);
String typeAsString = attributes.getValue(NODE_TYPE);
if (name == null) throw new SAXException("Name attribute is required in node tags.");
if (prototypeId == null) throw new SAXException("Prototype attribute is required in node tags.");
Class dataClass = null;
if (typeAsString != null) {
try {
dataClass = Class.forName(typeAsString);
} catch (ClassNotFoundException e) {
throw new SAXException("Given type " + typeAsString + " not found.");
}
}
// Switch between relative and long identifiers.
// Long identifiers (e.g. "polygraph.rect") contain both a library and name and should be looked up using the manager.
// Only exported nodes will qualify.
// Short identifiers (e.g. "beta") contain only a name and are in the same library as this node.
// They should be looked up using the library. These can be non-exported nodes as well.
Node prototype;
if (prototypeId.contains(".")) {
// Long identifier
prototype = manager.getNode(prototypeId);
} else {
// Short identifier
prototype = library.getRootNode().getChild(prototypeId);
}
if (prototype == null) throw new SAXException("Unknown prototype " + prototypeId + " for node " + name);
// Create the child at the root of the node library or the current parent
Node newNode;
if (currentNode == null) {
newNode = library.getRootNode().create(prototype, name, dataClass);
} else {
newNode = currentNode.create(prototype, name, dataClass);
}
// Parse additional node flags.
String x = attributes.getValue(NODE_X);
String y = attributes.getValue(NODE_Y);
if (x != null)
newNode.setX(Double.parseDouble(x));
if (y != null)
newNode.setY(Double.parseDouble(y));
if ("true".equals(attributes.getValue(NODE_RENDERED)))
newNode.setRendered();
if ("true".equals(attributes.getValue(NODE_EXPORTED)))
newNode.setExported(true);
// Go down into the current node; this will now become the current network.
currentNode = newNode;
}
private void startDescriptionTag(Attributes attributes) throws SAXException {
if (currentNode == null) throw new SAXException("Description tag encountered without a current node.");
state = ParseState.IN_DESCRIPTION;
characterData = new StringBuffer();
}
private void setDescription(String description) throws SAXException {
if (currentNode == null) throw new SAXException("Description tag ended without a current node.");
currentNode.setDescription(description);
}
private void startParameterTag(Attributes attributes) throws SAXException {
String name = attributes.getValue(PARAMETER_NAME);
String typeAsString = attributes.getValue(PARAMETER_TYPE);
if (currentNode == null) throw new SAXException("Parameter tag encountered without a current node.");
if (name == null)
throw new SAXException("Name is required for parameter on node '" + currentNode.getName() + "'.");
if (typeAsString == null) {
// No type attribute was given, so the parameter should already exist.
currentParameter = currentNode.getParameter(name);
if (currentParameter == null)
throw new SAXException("Parameter '" + name + "' for node '" + currentNode.getName() + "' does not exist.");
} else {
Parameter.Type type = Parameter.Type.valueOf(typeAsString.toUpperCase(Locale.US));
if (currentNode.hasParameter(name)) {
currentParameter = currentNode.getParameter(name);
currentParameter.setType(type);
} else {
// Type was given, so this is a new parameter.
currentParameter = currentNode.addParameter(name, type);
}
}
// Parse parameter attributes.
String widget = attributes.getValue(PARAMETER_WIDGET);
String label = attributes.getValue(PARAMETER_LABEL);
String helpText = attributes.getValue(PARAMETER_HELP_TEXT);
String displayLevel = attributes.getValue(PARAMETER_DISPLAY_LEVEL);
String enableExpression = attributes.getValue(PARAMETER_ENABLE_EXPRESSION);
String boundingMethod = attributes.getValue(PARAMETER_BOUNDING_METHOD);
String minimumValue = attributes.getValue(PARAMETER_MINIMUM_VALUE);
String maximumValue = attributes.getValue(PARAMETER_MAXIMUM_VALUE);
if (widget != null)
currentParameter.setWidget(Parameter.Widget.valueOf(widget.toUpperCase(Locale.US)));
if (label != null)
currentParameter.setLabel(label);
if (helpText != null)
currentParameter.setHelpText(helpText);
if (displayLevel != null)
currentParameter.setDisplayLevel(Parameter.DisplayLevel.valueOf(displayLevel.toUpperCase(Locale.US)));
if (enableExpression != null)
currentParameter.setEnableExpression(enableExpression);
if (boundingMethod != null)
currentParameter.setBoundingMethod(Parameter.BoundingMethod.valueOf(boundingMethod.toUpperCase(Locale.US)));
if (minimumValue != null)
currentParameter.setMinimumValue(Float.parseFloat(minimumValue));
if (maximumValue != null)
currentParameter.setMaximumValue(Float.parseFloat(maximumValue));
}
/**
* Parse the value tag. This tag is inside of the param tag.
*
* @param attributes tag attributes
* @throws SAXException if the current parameter is null or a code parameter has no or invalid value type.
*/
private void startValueTag(Attributes attributes) throws SAXException {
if (currentParameter == null) throw new SAXException("Value tag encountered without current parameter.");
// If the prototype parameter has an expression clear it, or the node will fail to load.
if (currentParameter.hasExpression())
currentParameter.clearExpression();
state = ParseState.IN_VALUE;
characterData = new StringBuffer();
// The value tag should be empty except when the parameter type is code.
// Then the value tag has a type attribute that specifies the code type.
if (currentParameter.getType() != Parameter.Type.CODE) return;
String type = attributes.getValue(VALUE_TYPE);
if (type == null) throw new SAXException("Type attribute is required in code type parameters.");
try {
currentCodeType = CodeType.valueOf(type.toUpperCase(Locale.US));
} catch (IllegalArgumentException e) {
throw new SAXException("Invalid type attribute in code tag: should be python or java, not " + type + ".");
}
}
/**
* Sets the value on the current parameter.
*
* @param valueAsString the value of the parameter, to be parsed.
* @throws org.xml.sax.SAXException when there is no current node, if parameter was not found or if the value could not be parsed.
*/
private void setValue(String valueAsString) throws SAXException {
if (currentParameter == null) throw new SAXException("There is no current parameter.");
Object value;
if (currentParameter.getType() == Parameter.Type.CODE) {
value = parseCode(valueAsString);
} else {
try {
value = currentParameter.parseValue(valueAsString);
} catch (IllegalArgumentException e) {
throw new SAXException(currentParameter.getAbsolutePath() + ": could not parse value '" + valueAsString + "'", e);
}
}
try {
currentParameter.setValue(value);
} catch (IllegalArgumentException e) {
throw new SAXException(currentParameter.getAbsolutePath() + ": value '" + valueAsString + "' invalid for parameter", e);
}
}
/**
* Parse the expression tag. This tag is inside of the param tag.
*
* @param attributes tag attributes
* @throws SAXException if the current parameter is null or a code parameter has no or invalid value type.
*/
private void startExpressionTag(Attributes attributes) throws SAXException {
if (currentParameter == null) throw new SAXException("Expression tag encountered without current parameter.");
state = ParseState.IN_EXPRESSION;
characterData = new StringBuffer();
}
/**
* Parse the expression tag. This tag is inside of the param tag.
*
* @param attributes tag attributes
* @throws SAXException if the current parameter is null or a code parameter has no or invalid value type.
*/
private void startMenuTag(Attributes attributes) throws SAXException {
if (currentParameter == null) throw new SAXException("Menu tag encountered without current parameter.");
state = ParseState.IN_MENU;
String key = attributes.getValue(MENU_KEY);
if (key == null)
throw new SAXException("Attribute key for menu tag cannot be null.");
currentMenuKey = key;
characterData = new StringBuffer();
}
/**
* Sets the menu item on the current parameter.
*
* The menu key was already set as an attribute on the menu start tag.
*
* @param label the character data for the menu label.
*/
private void setMenuItem(String label) {
if (currentMenuKey == null) throw new AssertionError("Menu tag ends, but menu key is null.");
currentParameter.addMenuItem(currentMenuKey, label);
}
/**
* Parses the given source code and returns a new NodeCode object of the correct type.
*
* This method assumes that the currentCodeType is set.
*
* @param source the source code
* @return a NodeCode object
* @throws org.xml.sax.SAXException when there is no current node, if parameter was not found or if the value could not be parsed.
*/
private NodeCode parseCode(String source) throws SAXException {
if (currentCodeType == CodeType.PYTHON) {
return new PythonCode(source);
} else if (currentCodeType == CodeType.JAVA) {
throw new SAXException("Support for Java code is not implemented yet.");
} else {
throw new SAXException("Invalid code type.");
}
}
/**
* Sets the expression on the current parameter.
*
* Expressions are not set directly, because all dependencies can not always be met directly.
* So expressions are stored in a temporary map. When the whole document is parsed, all expressions will be set,
* which will also set the correct dependencies.
*
* @param expression the expression string
* @throws org.xml.sax.SAXException when there is no current node or if parameter was not found.
*/
private void setTemporaryExpression(String expression) throws SAXException {
if (currentParameter == null) throw new SAXException("There is no current parameter.");
expressionMap.put(currentParameter, expression);
}
private void startPortTag(Attributes attributes) throws SAXException {
String name = attributes.getValue(PORT_NAME);
String cardinalityAsString = attributes.getValue(PORT_CARDINALITY);
if (name == null)
throw new SAXException("Name is required for port on node '" + currentNode.getName() + "'.");
Port.Cardinality cardinality = Port.Cardinality.SINGLE;
if (cardinalityAsString != null) {
try {
cardinality = Port.Cardinality.valueOf(cardinalityAsString.toUpperCase(Locale.US));
} catch (IllegalArgumentException e) {
throw new SAXException("Invalid cardinality attribute in port tag: should be single or multiple, not " + cardinalityAsString + ".");
}
}
currentNode.addPort(name, cardinality);
}
private void startConnectionTag(Attributes attributes) throws SAXException {
// output node identifier, without package
String outputAsString = attributes.getValue(CONNECTION_OUTPUT);
// input node identifier, without package
String inputAsString = attributes.getValue(CONNECTION_INPUT);
// input port identifier
String portAsString = attributes.getValue(CONNECTION_PORT);
String currentNodeString = currentNode == null ? "" : currentNode.getName();
if (outputAsString == null)
throw new SAXException("Output is required for connection in node '" + currentNodeString + "'.");
if (inputAsString == null)
throw new SAXException("Input is required for connection in node '" + currentNodeString + "'.");
if (portAsString == null)
throw new SAXException("Port is required for connection in node '" + currentNodeString + "'.");
Node output, input;
if (currentNode == null) {
output = rootNode.getChild(outputAsString);
input = rootNode.getChild(inputAsString);
} else {
output = currentNode.getChild(outputAsString);
input = currentNode.getChild(inputAsString);
}
if (output == null)
throw new SAXException("Output node '" + outputAsString + "' does not exist.");
if (input == null)
throw new SAXException("Input node '" + inputAsString + "' does not exist.");
Port port = input.getPort(portAsString);
if (port == null)
throw new SAXException("Port '" + portAsString + "' on node '" + inputAsString + "' does not exist.");
port.connect(output);
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
switch (state) {
case IN_CODE:
if (currentCodeType == null)
throw new SAXException("Code encountered, but no current code type.");
break;
case IN_DESCRIPTION:
if (currentNode == null)
throw new SAXException("Description encountered, but no current node.");
break;
case IN_VALUE:
if (currentParameter == null)
throw new SAXException("Value encountered, but no current parameter.");
break;
case IN_EXPRESSION:
if (currentParameter == null)
throw new SAXException("Expression encountered, but no current parameter.");
break;
case IN_MENU:
if (currentParameter == null)
throw new SAXException("Menu encountered, but no current parameter.");
break;
default:
// Bail out when we don't recognize this state.
return;
}
// We have a valid character state, so we can safely append to characterData.
characterData.append(ch, start, length);
}
}