package nodebox.node;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Files;
import nodebox.graphics.Point;
import nodebox.util.LoadException;
import org.python.google.common.collect.ImmutableList;
import org.w3c.dom.*;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
/**
* Helper class that contains all NodeLibrary upgrade migrations.
*/
public class NodeLibraryUpgrades {
private static final Pattern formatVersionPattern = Pattern.compile("formatVersion=['\"]([\\d\\.]+)['\"]");
private static Map<String, Method> upgradeMap = new HashMap<String, Method>();
/**
* Lookup an upgrade method by name.
* <p/>
* This method is not compatible
*
* @param methodName The upgrade method name.
* @return The Method object, to be invoked.
*/
private static Method upgradeMethod(String methodName) {
try {
return NodeLibraryUpgrades.class.getMethod(methodName, String.class);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
static {
upgradeMap.put("1.0", upgradeMethod("upgrade1to2"));
upgradeMap.put("2", upgradeMethod("upgrade2to3"));
upgradeMap.put("3", upgradeMethod("upgrade3to4"));
upgradeMap.put("4", upgradeMethod("upgrade4to5"));
upgradeMap.put("5", upgradeMethod("upgrade5to6"));
upgradeMap.put("6", upgradeMethod("upgrade6to7"));
upgradeMap.put("7", upgradeMethod("upgrade7to8"));
upgradeMap.put("8", upgradeMethod("upgrade8to9"));
upgradeMap.put("9", upgradeMethod("upgrade9to10"));
upgradeMap.put("10", upgradeMethod("upgrade10to11"));
upgradeMap.put("11", upgradeMethod("upgrade11to12"));
upgradeMap.put("12", upgradeMethod("upgrade12to13"));
upgradeMap.put("13", upgradeMethod("upgrade13to14"));
upgradeMap.put("14", upgradeMethod("upgrade14to15"));
upgradeMap.put("15", upgradeMethod("upgrade15to16"));
upgradeMap.put("16", upgradeMethod("upgrade16to17"));
upgradeMap.put("17", upgradeMethod("upgrade17to18"));
upgradeMap.put("18", upgradeMethod("upgrade18to19"));
upgradeMap.put("19", upgradeMethod("upgrade19to20"));
}
public static String parseFormatVersion(String xml) {
Matcher m = formatVersionPattern.matcher(xml);
if (!m.find()) throw new RuntimeException("Invalid NodeBox file: " + xml);
return m.group(1);
}
private static String readFile(File file) {
try {
return Files.toString(file, Charsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Upgrade the given file to the latest library version. The file is supposed to be a NDBX file.
* <p/>
* It is harmless to pass in current version NDBX files.
*
* @param file The .ndbx file to upgrade.
* @return An upgrade result, containing warnings, correct XML code and a NodeLibrary object.
* @throws nodebox.util.LoadException If the upgrade fails for some reason.
*/
public static UpgradeResult upgrade(File file) throws LoadException {
return upgradeTo(file, NodeLibrary.CURRENT_FORMAT_VERSION);
}
/**
* Upgrade the given file to the target version. The file is supposed to be a NDBX file.
* <p/>
* It is harmless to pass in current version NDBX files.
*
* @param file The .ndbx file to upgrade.
* @return An upgrade result, containing warnings, correct XML code and a NodeLibrary object.
* @throws LoadException If the upgrade fails for some reason.
*/
public static UpgradeResult upgradeTo(File file, String targetVersion) throws LoadException {
String currentXml = readFile(file);
String currentVersion = parseFormatVersion(currentXml);
ArrayList<String> warnings = new ArrayList<String>();
// Avoid upgrades getting stuck in an infinite loop.
int tries = 0;
if (currentVersion.equals("0.9")) {
throw new LoadException(file, "This is a NodeBox 2 file. Download NodeBox 2 from http://beta.nodebox.net/");
}
while (!currentVersion.equals(targetVersion) && tries < 100) {
Method upgradeMethod = upgradeMap.get(currentVersion);
if (upgradeMethod == null) {
throw new LoadException(file, "Unsupported version " + currentVersion + ": this file is too new. Try downloading a new version of NodeBox from http://nodebox.net/download/");
}
try {
UpgradeStringResult result = (UpgradeStringResult) upgradeMethod.invoke(null, currentXml);
warnings.addAll(result.warnings);
currentXml = result.xml;
} catch (Exception e) {
throw new LoadException(file, "Upgrading to " + currentVersion + " failed.", e);
}
currentVersion = parseFormatVersion(currentXml);
tries++;
}
if (tries >= 100) {
throw new LoadException(file, "Got stuck in an infinite loop when trying to upgrade from " + currentVersion);
}
return new UpgradeResult(file, currentXml, warnings);
}
public static UpgradeStringResult upgrade1to2(String inputXml) throws LoadException {
// Version 2: Vertical node networks
// 1. Rotate all nodes 90 degrees by reversing X and Y positions.
// 2. Convert from pixel units to grid units by dividing by GRID_CELL_SIZE.
final int GRID_CELL_SIZE = 48;
UpgradeOp verticalNodesOp = new UpgradeOp() {
@Override
public void apply(Element e) {
if (!e.getTagName().equals("node")) return;
Attr position = e.getAttributeNode("position");
if (position == null) return;
Point pt = Point.valueOf(position.getValue());
Point reversedPoint = new Point(pt.y, pt.x);
Point gridPoint = new Point(Math.round(reversedPoint.x / GRID_CELL_SIZE) * 3, Math.round(reversedPoint.y / GRID_CELL_SIZE));
position.setValue(String.valueOf(gridPoint));
}
@Override
public void end(Element root) {
addWarning("Nodes have been rotated. Your network will look different.");
}
};
return transformXml(inputXml, "2", verticalNodesOp);
}
public static UpgradeStringResult upgrade2to3(String inputXml) throws LoadException {
// Version 3: Rename math.to_integer to math.round.
UpgradeOp changePrototypeOp = new ChangePrototypeOp("math.to_integer", "math.round");
UpgradeOp renameOp = new RenameNodeOp("to_integer", "round");
return transformXml(inputXml, "3", changePrototypeOp, renameOp);
}
public static UpgradeStringResult upgrade3to4(String inputXml) throws LoadException {
// Version 4: Convert corevector.to_points nodes to corevector.point nodes.
UpgradeOp changePrototypeOp = new ChangePrototypeOp("corevector.to_points", "corevector.point");
UpgradeOp renameOp = new RenameNodeOp("to_points", "point");
// todo: write test code for renamePortOp addition.
UpgradeOp renamePortOp = new RenamePortOp("corevector.to_points", "shape", "value");
return transformXml(inputXml, "4", renamePortOp, changePrototypeOp, renameOp);
}
public static UpgradeStringResult upgrade4to5(String inputXml) throws LoadException {
// Version 5: The corevector.textpath node loses the height port.
UpgradeOp removeInputOp = new RemoveInputOp("corevector.textpath", "height");
return transformXml(inputXml, "5", removeInputOp);
}
public static UpgradeStringResult upgrade5to6(String inputXml) throws LoadException {
// Version 6: Change delete.delete_selected boolean to menu options.
Map<String, String> mappings = ImmutableMap.of("true", "selected", "false", "non-selected");
UpgradeOp renamePortOp = new RenamePortOp("corevector.delete", "delete_selected", "operation");
UpgradeOp changePortTypeOp = new ChangePortTypeOp("corevector.delete", "operation", "string", mappings);
return transformXml(inputXml, "6", renamePortOp, changePortTypeOp);
}
public static UpgradeStringResult upgrade6to7(String inputXml) throws LoadException {
// Version 7: Replace instances of list.filter with list.cull.
UpgradeOp changePrototypeOp = new ChangePrototypeOp("list.filter", "list.cull");
UpgradeOp renameOp = new RenameNodeOp("filter", "cull");
return transformXml(inputXml, "7", changePrototypeOp, renameOp);
}
public static UpgradeStringResult upgrade7to8(String inputXml) throws LoadException {
// Version 8: The corevector.point_on_path node loses the range port.
UpgradeOp removeInputOp = new RemoveInputOp("corevector.point_on_path", "range");
return transformXml(inputXml, "8", removeInputOp);
}
public static UpgradeStringResult upgrade8to9(String inputXml) throws LoadException {
// Version 9: corevector's resample_by_amount and resample_by_length nodes
// are replaced by the more generic resample node.
UpgradeOp addInputOp1 = new AddInputOp("corevector.resample_by_amount", "method", "string", "amount");
UpgradeOp changePrototypeOp1 = new ChangePrototypeOp("corevector.resample_by_amount", "corevector.resample");
UpgradeOp renameOp1 = new RenameNodeOp("resample_by_amount", "resample");
UpgradeOp addInputOp2 = new AddInputOp("corevector.resample_by_length", "method", "string", "length");
UpgradeOp changePrototypeOp2 = new ChangePrototypeOp("corevector.resample_by_length", "corevector.resample");
UpgradeOp renameOp2 = new RenameNodeOp("resample_by_length", "resample");
return transformXml(inputXml, "9",
addInputOp1, changePrototypeOp1, renameOp1,
addInputOp2, changePrototypeOp2, renameOp2);
}
public static UpgradeStringResult upgrade9to10(String inputXml) throws LoadException {
// Version 10: corevector's wiggle_contours, wiggle_paths and wiggle_points nodes
// are replaced by the more generic wiggle node.
UpgradeOp addInputOp1 = new AddInputOp("corevector.wiggle_contours", "scope", "string", "contours");
UpgradeOp changePrototypeOp1 = new ChangePrototypeOp("corevector.wiggle_contours", "corevector.wiggle");
UpgradeOp renameOp1 = new RenameNodeOp("wiggle_contours", "wiggle");
UpgradeOp addInputOp2 = new AddInputOp("corevector.wiggle_paths", "scope", "string", "paths");
UpgradeOp changePrototypeOp2 = new ChangePrototypeOp("corevector.wiggle_paths", "corevector.wiggle");
UpgradeOp renameOp2 = new RenameNodeOp("wiggle_paths", "wiggle");
UpgradeOp addInputOp3 = new AddInputOp("corevector.wiggle_points", "scope", "string", "points");
UpgradeOp changePrototypeOp3 = new ChangePrototypeOp("corevector.wiggle_points", "corevector.wiggle");
UpgradeOp renameOp3 = new RenameNodeOp("wiggle_points", "wiggle");
return transformXml(inputXml, "10",
addInputOp1, changePrototypeOp1, renameOp1,
addInputOp2, changePrototypeOp2, renameOp2,
addInputOp3, changePrototypeOp3, renameOp3);
}
public static UpgradeStringResult upgrade10to11(String inputXml) throws LoadException {
UpgradeOp removeNodeOp = new RemoveNodeOp("corevector.draw_path");
return transformXml(inputXml, "11", removeNodeOp);
}
public static UpgradeStringResult upgrade11to12(String inputXml) throws LoadException {
UpgradeOp renamePortOp1 = new RenamePortOp("corevector.shape_on_path", "template", "path");
UpgradeOp renamePortOp2 = new RenamePortOp("corevector.shape_on_path", "dist", "spacing");
UpgradeOp renamePortOp3 = new RenamePortOp("corevector.shape_on_path", "start", "margin");
return transformXml(inputXml, "12", renamePortOp1, renamePortOp2, renamePortOp3);
}
public static UpgradeStringResult upgrade12to13(String inputXml) throws LoadException {
UpgradeOp renamePortOp1 = new RenamePortOp("corevector.text_on_path", "shape", "path");
UpgradeOp renamePortOp2 = new RenamePortOp("corevector.text_on_path", "position", "margin");
UpgradeOp renamePortOp3 = new RenamePortOp("corevector.text_on_path", "offset", "baseline_offset");
UpgradeOp removeInputOp = new RemoveInputOp("corevector.text_on_path", "keep_geometry");
return transformXml(inputXml, "13", renamePortOp1, renamePortOp2, renamePortOp3, removeInputOp);
}
public static UpgradeStringResult upgrade13to14(String inputXml) throws LoadException {
UpgradeOp renamePortOp1 = new RenamePortOp("math.wave", "speed", "period");
UpgradeOp renamePortOp2 = new RenamePortOp("math.wave", "frame", "offset");
return transformXml(inputXml, "14", renamePortOp1, renamePortOp2);
}
public static UpgradeStringResult upgrade14to15(String inputXml) throws LoadException {
UpgradeOp renameNodeOp = new RenameNodeOp("make_strings", "split");
return transformXml(inputXml, "15", renameNodeOp);
}
public static UpgradeStringResult upgrade15to16(String inputXml) throws LoadException {
// Version 16: 'network' and 'node' are reserved names. Nodes with those names have to be renamed.
// Besides this, only the top level node in any network is allowed to have the name 'root'.
UpgradeOp renameNodeOp1 = new ExactRenameNodeOp("network", "network");
UpgradeOp renameNodeOp2 = new ExactRenameNodeOp("node", "node");
ExactRenameNodeOp renameNodeOp3 = new ExactRenameNodeOp("root", "node");
renameNodeOp3.skipRootNode();
UpgradeOp addAttributeOp = new AddAttributeOp("corevector.geonet", "outputType", "geometry");
UpgradeOp changePrototypeOp = new ChangePrototypeOp("corevector.geonet", "core.network");
return transformXml(inputXml, "16", renameNodeOp1, renameNodeOp2, renameNodeOp3, addAttributeOp, changePrototypeOp);
}
public static UpgradeStringResult upgrade16to17(String inputXml) throws LoadException {
UpgradeOp convertOSCPropertyOp = new ConvertOSCPropertyFormatOp();
return transformXml(inputXml, "17", convertOSCPropertyOp);
}
public static UpgradeStringResult upgrade17to18(String inputXml) throws LoadException {
// Version 18: "switch" and "combine" nodes have more ports. This doesn't change anything in the file
// but does make the files backward-incompatible.
UpgradeOp convertOSCPropertyOp = new ConvertOSCPropertyFormatOp();
return transformXml(inputXml, "18");
}
public static UpgradeStringResult upgrade18to19(String inputXml) throws LoadException {
// Version 19: audioplayer devices previously had their default device name set to "audioplayer1".
// This has changed to "audio1", so to have backward compatibility we have to make sure the
// old name is set explicitly and not derived from the prototype.
UpgradeOp renameDeviceNameOp1 = new SetOldDefaultAudioDeviceNameOp("device.audio_analysis", "device_name", "audioplayer1");
UpgradeOp renameDeviceNameOp2 = new SetOldDefaultAudioDeviceNameOp("device.audio_wave", "device_name", "audioplayer1");
UpgradeOp renameDeviceNameOp3 = new SetOldDefaultAudioDeviceNameOp("device.beat_detect", "device_name", "audioplayer1");
return transformXml(inputXml, "19", renameDeviceNameOp1, renameDeviceNameOp2, renameDeviceNameOp3);
}
public static UpgradeStringResult upgrade19to20(String inputXml) throws LoadException {
UpgradeOp renameDevicePropertyOp1 = new ConvertDevicePropertyNameOp("osc", "autostart", "sync_with_timeline");
UpgradeOp renameDevicePropertyOp2 = new ConvertDevicePropertyNameOp("audioplayer", "autostart", "sync_with_timeline");
UpgradeOp renameDevicePropertyOp3 = new ConvertDevicePropertyNameOp("audioinput", "autostart", "sync_with_timeline");
return transformXml(inputXml, "20", renameDevicePropertyOp1, renameDevicePropertyOp2, renameDevicePropertyOp3);
}
private static List<Node> childNodes(Node parent) {
ArrayList<Node> childNodes = new ArrayList<Node>();
NodeList children = parent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
childNodes.add(children.item(i));
}
return childNodes;
}
private static List<Element> childElements(Node parent) {
ArrayList<Element> childElements = new ArrayList<Element>();
NodeList children = parent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (node instanceof Element) {
childElements.add((Element) node);
}
}
return childElements;
}
/**
* Return direct descendant elements of parent node with the given childName.
*/
private static List<Element> childElementsWithName(Node parent, String childName) {
ArrayList<Element> childElements = new ArrayList<Element>();
NodeList children = parent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (node instanceof Element && ((Element) node).getTagName().equals(childName)) {
childElements.add((Element) node);
}
}
return childElements;
}
private static Set<String> getChildNodeNames(Element parent) {
HashSet<String> names = new HashSet<String>();
for (Element e : childElementsWithName(parent, "node")) {
names.add(e.getAttribute("name"));
}
return names;
}
private static String uniqueName(String prefix, Set<String> existingNames) {
int counter = 1;
while (true) {
String suggestedName = prefix + counter;
if (!existingNames.contains(suggestedName)) {
return suggestedName;
}
counter++;
}
}
private static void renameRenderedChildReference(Element element, String oldNodeName, String newNodeName) {
Attr renderedChildReference = element.getAttributeNode("renderedChild");
if (renderedChildReference == null) return;
String oldRenderedChild = renderedChildReference.getValue();
if (oldRenderedChild.equals(oldNodeName)) {
if (newNodeName == null || newNodeName.length() == 0)
element.removeAttributeNode(renderedChildReference);
else
renderedChildReference.setValue(newNodeName);
}
}
private static void renamePortReference(List<Element> elements, String attributeName, String oldNodeName, String newNodeName) {
for (Element c : elements) {
Attr portReference = c.getAttributeNode(attributeName);
if (portReference == null) continue;
Iterator<String> portRefIterator = NodeLibrary.PORT_NAME_SPLITTER.split(portReference.getValue()).iterator();
String nodeName = portRefIterator.next();
String portName = portRefIterator.next();
if (oldNodeName.equals(nodeName)) {
portReference.setValue(String.format("%s.%s", newNodeName, portName));
}
}
}
private static void renamePortInNodeList(List<Element> elements, String attributeName, String nodeName, String oldPortName, String newPortName) {
for (Element c : elements) {
Attr portReference = c.getAttributeNode(attributeName);
if (portReference == null) continue;
Iterator<String> portRefIterator = NodeLibrary.PORT_NAME_SPLITTER.split(portReference.getValue()).iterator();
String nodeRef = portRefIterator.next();
String portRef = portRefIterator.next();
if (nodeRef.equals(nodeName) && portRef.equals(oldPortName)) {
portReference.setValue(String.format("%s.%s", nodeName, newPortName));
}
}
}
private static void renameNodeReference(List<Element> elements, String attributeName, String oldNodeName, String newNodeName) {
for (Element c : elements) {
Attr nodeRef = c.getAttributeNode(attributeName);
String nodeName = nodeRef.getValue();
if (oldNodeName.equals(nodeName)) {
nodeRef.setValue(newNodeName);
}
}
}
private static void removeNodeInput(Element node, String input) {
for (Element port : childElementsWithName(node, "port")) {
Attr nameAttr = port.getAttributeNode("name");
String portName = nameAttr.getValue();
if (portName.equals(input)) {
node.removeChild(port);
}
}
if (node.getAttributeNode("name") != null) {
Element parent = (Element) node.getParentNode();
String child = node.getAttributeNode("name").getValue();
removeConnection(parent, child, input);
String publishedInput = getParentPublishedInput(parent, child, input);
if (publishedInput != null)
removeNodeInput(parent, publishedInput);
}
}
private static void removeConnection(Element parent, String child, String input) {
for (Element conn : childElementsWithName(parent, "conn")) {
String inputPort = conn.getAttribute("input");
if (inputPort.equals(String.format("%s.%s", child, input))) {
parent.removeChild(conn);
}
}
}
private static void removeConnections(Element parent, String child) {
for (Element conn : childElementsWithName(parent, "conn")) {
String inputPort = conn.getAttribute("input");
String inputNode = inputPort.split("\\.")[0];
String outputNode = conn.getAttribute("output");
if (inputNode.equals(child) || outputNode.equals(child))
parent.removeChild(conn);
}
}
private static String getParentPublishedInput(Element parent, String child, String input) {
for (Element port : childElementsWithName(parent, "port")) {
Attr childRef = port.getAttributeNode("childReference");
if (childRef != null && childRef.getValue().equals(String.format("%s.%s", child, input))) {
return port.getAttribute("name");
}
}
return null;
}
private static List<String> getParentPublishedInputs(Element parent, String child) {
ImmutableList.Builder<String> builder = ImmutableList.builder();
for (Element port : childElementsWithName(parent, "port")) {
Attr childRef = port.getAttributeNode("childReference");
if (childRef != null && childRef.getValue().split("\\.")[0].equals(child)) {
builder.add(port.getAttribute("name"));
}
}
return builder.build();
}
private static boolean isNodeWithPrototype(Element e, String nodePrototype) {
if (e.getTagName().equals("node")) {
Attr prototype = e.getAttributeNode("prototype");
if (prototype != null && prototype.getValue().equals(nodePrototype)) {
return true;
}
}
return false;
}
private static Element portWithName(Element nodeElement, String portName) {
for (Element port : childElementsWithName(nodeElement, "port")) {
if (port.getAttribute("name").equals(portName)) {
return port;
}
}
return null;
}
private static UpgradeStringResult transformXml(String xml, String newFormatVersion, UpgradeOp... ops) {
try {
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder;
builder = builderFactory.newDocumentBuilder();
Document document = builder.parse(new InputSource(new StringReader(xml)));
// Check that this is a NodeBox document and set the new formatVersion.
Element root = document.getDocumentElement();
checkArgument(root.getTagName().equals("ndbx"), "This is not a valid NodeBox document.");
root.setAttribute("formatVersion", newFormatVersion);
// Loop through all upgrade operations.
ArrayList<String> warnings = new ArrayList<String>();
for (UpgradeOp op : ops) {
op.start(root);
transformXmlRecursive(document.getDocumentElement(), op);
op.end(root);
warnings.addAll(op.getWarnings());
}
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
DOMSource source = new DOMSource(document);
StringWriter sw = new StringWriter();
StreamResult result = new StreamResult(sw);
transformer.transform(source, result);
return new UpgradeStringResult(sw.toString(), warnings);
} catch (Exception e) {
throw new RuntimeException("Error while upgrading to " + newFormatVersion + ".", e);
}
}
private static void transformXmlRecursive(Element e, UpgradeOp op) {
op.apply(e);
for (Element child : childElements(e)) {
transformXmlRecursive(child, op);
}
}
private static abstract class UpgradeOp {
private List<String> warnings = new ArrayList<String>();
public void start(Element root) {
}
public void end(Element root) {
}
public abstract void apply(Element e);
public void addWarning(String warning) {
warnings.add(warning);
}
public List<String> getWarnings() {
return warnings;
}
}
private static class ChangePrototypeOp extends UpgradeOp {
private String oldPrototype;
private String newPrototype;
private ChangePrototypeOp(String oldPrototype, String newPrototype) {
this.oldPrototype = oldPrototype;
this.newPrototype = newPrototype;
}
public void apply(Element e) {
if (isNodeWithPrototype(e, oldPrototype)) {
Attr prototype = e.getAttributeNode("prototype");
prototype.setValue(newPrototype);
}
}
}
private static class SetOldDefaultAudioDeviceNameOp extends UpgradeOp {
private String prototype;
private String portName;
private String deviceName;
private SetOldDefaultAudioDeviceNameOp(String prototype, String portName, String deviceName) {
this.prototype = prototype;
this.portName = portName;
this.deviceName = deviceName;
}
public void apply(Element e) {
if (isNodeWithPrototype(e, prototype)) {
Element port = portWithName(e, portName);
if (port == null) {
port = e.getOwnerDocument().createElement("port");
port.setAttribute("name", portName);
port.setAttribute("type", "string");
port.setAttribute("value", deviceName);
e.appendChild(port);
}
}
}
}
private static class AddAttributeOp extends UpgradeOp {
private String prototype;
private String attributeName;
private String attributeValue;
private AddAttributeOp(String prototype, String attributeName, String attributeValue) {
this.prototype = prototype;
this.attributeName = attributeName;
this.attributeValue = attributeValue;
}
public void apply(Element e) {
if (isNodeWithPrototype(e, prototype)) {
e.setAttribute(attributeName, attributeValue);
}
}
}
private static class ConvertOSCPropertyFormatOp extends UpgradeOp {
@Override
public void apply(Element e) {
if (e.getTagName().equals("property")) {
Element parent = (Element) e.getParentNode();
if (parent != null && parent.getTagName().equals("ndbx")) {
Attr name = e.getAttributeNode("name");
Attr value = e.getAttributeNode("value");
if (name != null && name.getValue().equals("oscPort")) {
if (value != null) {
Element device = e.getOwnerDocument().createElement("device");
device.setAttribute("name", "osc1");
device.setAttribute("type", "osc");
Element portProperty = e.getOwnerDocument().createElement("property");
portProperty.setAttribute("name", "port");
portProperty.setAttribute("value", value.getValue());
device.appendChild(portProperty);
Element autostartProperty = e.getOwnerDocument().createElement("property");
autostartProperty.setAttribute("name", "autostart");
autostartProperty.setAttribute("value", "true");
device.appendChild(autostartProperty);
parent.replaceChild(device, e);
} else {
parent.removeChild(e);
}
}
}
}
}
}
private static class ConvertDevicePropertyNameOp extends UpgradeOp {
private String deviceType;
private String oldPropertyName;
private String newPropertyName;
private ConvertDevicePropertyNameOp(String deviceType, String oldPropertyName, String newPropertyName) {
this.deviceType = deviceType;
this.oldPropertyName = oldPropertyName;
this.newPropertyName = newPropertyName;
}
@Override
public void apply(Element e) {
if (e.getTagName().equals("property")) {
Element parent = (Element) e.getParentNode();
if (parent != null && parent.getTagName().equals("device")) {
Attr type = parent.getAttributeNode("type");
if (type != null && type.getValue().equals(this.deviceType)) {
Attr name = e.getAttributeNode("name");
if (name != null && name.getValue().equals(oldPropertyName))
name.setValue(newPropertyName);
}
}
}
}
}
private static class RenameNodeOp extends UpgradeOp {
private String oldPrefix;
private String newPrefix;
private RenameNodeOp(String oldPrefix, String newPrefix) {
this.oldPrefix = oldPrefix;
this.newPrefix = newPrefix;
}
@Override
public void apply(Element e) {
if (e.getTagName().equals("node")) {
Attr name = e.getAttributeNode("name");
if (name != null && name.getValue().startsWith(oldPrefix)) {
String oldNodeName = name.getValue();
Set<String> childNames = getChildNodeNames((Element) e.getParentNode());
String newNodeName = uniqueName(newPrefix, childNames);
name.setValue(newNodeName);
Element parent = (Element) e.getParentNode();
renameRenderedChildReference(parent, oldNodeName, newNodeName);
List<Element> connections = childElementsWithName(parent, "conn");
renamePortReference(connections, "input", oldNodeName, newNodeName);
renameNodeReference(connections, "output", oldNodeName, newNodeName);
List<Element> ports = childElementsWithName(parent, "port");
renamePortReference(ports, "childReference", oldNodeName, newNodeName);
}
}
}
}
private static class ExactRenameNodeOp extends UpgradeOp {
private String oldNodeName;
private String newPrefix;
private boolean shouldSkipRoot = false;
private ExactRenameNodeOp(String oldNodeName, String newPrefix) {
this.oldNodeName = oldNodeName;
this.newPrefix = newPrefix;
}
private void skipRootNode() {
this.shouldSkipRoot = true;
}
@Override
public void apply(Element e) {
if (e.getTagName().equals("node")) {
if (shouldSkipRoot) {
Element parent = (Element) e.getParentNode();
if (parent != null && !parent.getTagName().equals("node"))
return;
}
Attr name = e.getAttributeNode("name");
if (name != null && name.getValue().equals(oldNodeName)) {
Set<String> childNames = getChildNodeNames((Element) e.getParentNode());
String newNodeName = uniqueName(newPrefix, childNames);
name.setValue(newNodeName);
Element parent = (Element) e.getParentNode();
renameRenderedChildReference(parent, oldNodeName, newNodeName);
List<Element> connections = childElementsWithName(parent, "conn");
renamePortReference(connections, "input", oldNodeName, newNodeName);
renameNodeReference(connections, "output", oldNodeName, newNodeName);
List<Element> ports = childElementsWithName(parent, "port");
renamePortReference(ports, "childReference", oldNodeName, newNodeName);
}
}
}
}
private static class RemoveInputOp extends UpgradeOp {
private String nodePrototype;
private String inputToRemove;
private RemoveInputOp(String nodePrototype, String inputToRemove) {
this.nodePrototype = nodePrototype;
this.inputToRemove = inputToRemove;
}
@Override
public void apply(Element e) {
if (isNodeWithPrototype(e, nodePrototype)) {
removeNodeInput(e, inputToRemove);
}
}
}
private static class RenamePortOp extends UpgradeOp {
private String nodePrototype;
private String oldPortName;
private String newPortName;
private RenamePortOp(String nodePrototype, String oldPortName, String newPortName) {
this.nodePrototype = nodePrototype;
this.oldPortName = oldPortName;
this.newPortName = newPortName;
}
@Override
public void apply(Element e) {
if (isNodeWithPrototype(e, nodePrototype)) {
String nodeName = e.getAttribute("name");
Element port = portWithName(e, oldPortName);
if (port != null)
port.setAttribute("name", newPortName);
Element parent = (Element) e.getParentNode();
List<Element> connections = childElementsWithName(parent, "conn");
renamePortInNodeList(connections, "input", nodeName, oldPortName, newPortName);
List<Element> ports = childElementsWithName(parent, "port");
renamePortInNodeList(ports, "childReference", nodeName, oldPortName, newPortName);
}
}
}
private static class ChangePortTypeOp extends UpgradeOp {
private String nodePrototype;
private String portName;
private String newType;
// The value mappings are strings, since that's what's stored in the XML file.
private Map<String, String> valueMappings;
public ChangePortTypeOp(String nodePrototype, String portName, String newType, Map<String, String> valueMappings) {
this.nodePrototype = nodePrototype;
this.portName = portName;
this.newType = newType;
this.valueMappings = valueMappings;
}
@Override
public void apply(Element e) {
if (isNodeWithPrototype(e, nodePrototype)) {
Element port = portWithName(e, portName);
if (port != null) {
Attr type = port.getAttributeNode("type");
type.setValue(newType);
Attr value = port.getAttributeNode("value");
if (value != null) {
String newValue = valueMappings.get(value.getValue());
checkState(newValue != null,
"Change port type (%s.%s -> %s): value %s not found in value mappings.",
nodePrototype, portName, newType, value.getValue());
value.setValue(newValue);
}
}
}
}
}
private static class AddInputOp extends UpgradeOp {
private String nodePrototype;
private String name;
private String type;
private String value;
private AddInputOp(String nodePrototype, String name, String type, String value) {
this.nodePrototype = nodePrototype;
this.name = name;
this.type = type;
this.value = value;
}
@Override
public void apply(Element e) {
if (isNodeWithPrototype(e, nodePrototype)) {
Element port = e.getOwnerDocument().createElement("port");
port.setAttribute("name", this.name);
port.setAttribute("type", this.type);
port.setAttribute("value", this.value);
e.appendChild(port);
}
}
}
private static class RemoveNodeOp extends UpgradeOp {
private String nodePrototype;
private List<String> removedNodes;
private RemoveNodeOp(String nodePrototype) {
this.nodePrototype = nodePrototype;
removedNodes = new ArrayList<String>();
}
@Override
public void apply(Element e) {
if (isNodeWithPrototype(e, nodePrototype)) {
Element parent = (Element) e.getParentNode();
String child = e.getAttributeNode("name").getValue();
removedNodes.add(child);
List<String> publishedInputs = getParentPublishedInputs(parent, child);
for (String publishedInput : publishedInputs)
removeNodeInput(parent, publishedInput);
removeConnections(parent, child);
renameRenderedChildReference(parent, child, null);
e.getParentNode().removeChild(e);
}
}
@Override
public void end(Element root) {
if (removedNodes.size() > 0)
addWarning(String.format("The '%s' node became obsolete, the following nodes in your network got removed: %s", nodePrototype, removedNodes));
}
}
private static class UpgradeStringResult {
private final String xml;
private final List<String> warnings;
private UpgradeStringResult(String xml, List<String> warnings) {
this.xml = xml;
this.warnings = warnings;
}
}
}