-
Notifications
You must be signed in to change notification settings - Fork 89
Expand file tree
/
Copy pathNDBXHandler.java
More file actions
527 lines (487 loc) · 23.5 KB
/
NDBXHandler.java
File metadata and controls
527 lines (487 loc) · 23.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
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.
* <p/>
* 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<Parameter, String> expressionMap = new HashMap<Parameter, String>();
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.
* <p/>
* 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<Parameter, String> 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.
* <p/>
* 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.
* <p/>
* 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.
* <p/>
* 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 ? "<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);
}
}