/*
* This file is part of NodeBox.
*
* Copyright (C) 2008 Frederik De Bleser (frederik@pandora.be)
*
* NodeBox 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.
*
* NodeBox 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 NodeBox. If not, see .
*/
package nodebox.node;
import nodebox.client.NodeBoxDocument;
import nodebox.graphics.Color;
import nodebox.graphics.Point;
import nodebox.handle.Handle;
import nodebox.util.StringUtils;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static nodebox.base.Preconditions.*;
/**
* A Node is a building block in a network and encapsulates specific functionality.
*
* The operation of the Node is specified through its parameters. The data that flows
* through the node passes through ports.
*
* Nodes can be nested using parent/child relationships. Then, you can connect them together.
* This allows for many processing possibilities, where you can connect several nodes together forming
* very complicated networks. Networks, in turn, can be rigged up to form sort of black-boxes, with some
* input parameters and an output parameter, so they form a Node themselves, that can be used to form
* even more complicated networks, etc.
*
* Central in this concept is the directed acyclic graph, or DAG. This is a graph where all the edges
* are directed, and no cycles can be formed, so you do not run into recursive loops. The vertexes of
* the graph are the nodes, and the edges are the connections between them.
*
* One of the vertexes in the graph is set as the rendered node, and from there on, the processing starts,
* working its way upwards in the network, processing other nodes (and their inputs) as they come along.
*/
public class Node implements NodeCode {
private static final Pattern NODE_NAME_PATTERN = Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]{0,29}$");
private static final Pattern DOUBLE_UNDERSCORE_PATTERN = Pattern.compile("^__.*$");
private static final Pattern RESERVED_WORD_PATTERN = Pattern.compile("^(node|network|root|context)$");
private static final Pattern NUMBER_AT_THE_END = Pattern.compile("^(.*?)(\\d*)$");
public static final String IMAGE_GENERIC = "__generic";
public static final String OUTPUT_PORT_NAME = "output";
public static final Node ROOT_NODE;
public enum Attribute {
LIBRARY, NAME, POSITION, EXPORT, DESCRIPTION, IMAGE, PARAMETER, PORT
}
static {
ROOT_NODE = new Node(NodeLibrary.BUILTINS, "root", Object.class);
ROOT_NODE.addParameter("_code", Parameter.Type.CODE, ROOT_NODE);
ROOT_NODE.addParameter("_handle", Parameter.Type.CODE, new JavaMethodWrapper(Node.class, "doNothing"));
ROOT_NODE.addParameter("_description", Parameter.Type.STRING, "Base node instance.");
ROOT_NODE.addParameter("_image", Parameter.Type.STRING, IMAGE_GENERIC);
NodeLibrary.BUILTINS.add(ROOT_NODE);
}
/**
* The name of this node.
*/
private String name;
/**
* The library this node is in.
*/
private NodeLibrary library;
/**
* The parent for this node.
*/
private Node parent;
/**
* The children of this node.
*/
private HashMap children = new HashMap();
/**
* The node's prototype. NodeBox uses prototype-based inheritance to blur the lines between classes and instances.
*/
private Node prototype;
/**
* The type of data that will be processed by this node.
*/
private Class dataClass;
/**
* Position of this node in the interface.
*/
private double x, y;
/**
* A flag that indicates whether this node is in need of processing.
* The dirty flag is set using markDirty and cleared while processing.
*/
private transient boolean dirty = true;
/**
* A flag that indicates that this node will be exported.
* This flag only has effect for nodes directly under the root node in a library.
*/
private boolean exported;
/**
* A map of all parameters.
*/
private LinkedHashMap parameters = new LinkedHashMap();
/**
* A map of all the data ports within the system.
*/
private LinkedHashMap ports = new LinkedHashMap();
/**
* The output port. This port will contain the processed data for this node.
*/
private Port outputPort;
/**
* The child node to render.
*/
private Node renderedChild;
/**
* All child connections within this node.
*/
private List connections = new ArrayList();
/**
* The processing error. Null if no error occurred during processing.
*/
private Throwable error;
//// Constructors ////
private Node(NodeLibrary library, String name, Class dataClass) {
assert library != null;
this.library = library;
this.name = name;
this.dataClass = dataClass;
this.outputPort = new Port(this, OUTPUT_PORT_NAME, Port.Direction.OUT);
}
//// Naming /////
public String getName() {
return name;
}
public void setName(String name) throws InvalidNameException {
if (this.name.equals(name)) return;
if (this.parent.children.containsKey(name))
throw new InvalidNameException(null, name, "The network already contains a node named " + name);
validateName(name);
this.parent.children.remove(this.name);
this.name = name;
this.parent.children.put(this.name, this);
getLibrary().fireNodeAttributeChanged(this, Attribute.NAME);
}
public NodeLibrary getLibrary() {
return library;
}
public String getIdentifier() {
return library + "." + name;
}
/**
* Get an identifier that is relative to the given node.
*
* This means that if the node and prototype are in the same library, the identifier
* is just the name of the prototype. Otherwise, the library name is added.
*
* @param relativeTo the instance this prototype is relative to
* @return a short or long identifier
*/
public String getRelativeIdentifier(Node relativeTo) {
if (relativeTo.library == library) {
return name;
} else {
return getIdentifier();
}
}
public String getDescription() {
return asString("_description");
}
public void setDescription(String description) {
setValue("_description", description);
getLibrary().fireNodeAttributeChanged(this, Attribute.DESCRIPTION);
}
public String getImage() {
return asString("_image");
}
public void setImage(String image) {
setValue("_image", image);
getLibrary().fireNodeAttributeChanged(this, Attribute.IMAGE);
}
/**
* Checks if the given name would be valid for this node.
*
* @param name the name to check.
* @throws InvalidNameException if the name was invalid.
*/
public static void validateName(String name) throws InvalidNameException {
Matcher m1 = NODE_NAME_PATTERN.matcher(name);
Matcher m2 = DOUBLE_UNDERSCORE_PATTERN.matcher(name);
Matcher m3 = RESERVED_WORD_PATTERN.matcher(name);
if (!m1.matches()) {
throw new InvalidNameException(null, name, "Names can only contain lowercase letters, numbers, and the underscore. Names cannot be longer than 29 characters.");
}
if (m2.matches()) {
throw new InvalidNameException(null, name, "Names starting with double underscore are reserved for internal use.");
}
if (m3.matches()) {
throw new InvalidNameException(null, name, "Names cannot be a reserved word (network, node, root).");
}
}
//// Parent/child relationship ////
public Node getParent() {
return parent;
}
public Node getRoot() {
Node n = this;
while (n.getParent() != null) {
n = n.getParent();
}
return n;
}
/**
* Reparent the node.
*
* This breaks all connections.
*
* @param parent the new parent
*/
public void setParent(Node parent) {
// This method is called indirectly by newInstance.
// newInstance has set the parent, but has not added it to
// the library yet. Therefore, we cannot do this.parent == parent,
// but need to check parent.contains()
if (parent != null && parent.containsChildNode(this)) return;
if (parent != null && parent.containsChildNode(name))
throw new InvalidNameException(this, name, "There is already a node named \"" + name + "\" in " + parent);
// Since this node will reside under a different parent, it can no longer maintain connections within
// the previous parent. Break all connections. We need to do this before the parent changes.
disconnect();
if (this.parent != null)
this.parent.remove(this);
this.parent = parent;
if (parent != null) {
parent.children.put(name, this);
// We're on the child node, so we need to fire the child added event
// on the parent with this child as the argument.
getLibrary().fireChildAdded(parent, this);
}
}
public boolean hasParent() {
return parent != null;
}
public boolean isLeaf() {
return isEmpty();
}
public boolean isEmpty() {
return children.isEmpty();
}
public int size() {
return children.size();
}
public void add(Node node) {
if (node == null)
throw new IllegalArgumentException("The node cannot be null.");
node.setParent(this);
}
/**
* Create a child node under this node from the given prototype.
* The name for this child is generated automatically.
*
* @param prototype the prototype node
* @return a new Node
*/
public Node create(Node prototype) {
if (prototype == null) throw new IllegalArgumentException("Prototype cannot be null.");
return create(prototype, null, null);
}
/**
* Create a child node under this node from the given prototype.
*
* @param prototype the prototype node
* @param name the name of the new node
* @return a new Node
*/
public Node create(Node prototype, String name) {
return create(prototype, name, null);
}
/**
* Create a child node under this node from the given prototype.
* The name for this child is generated automatically.
*
* @param prototype the prototype node
* @param dataClass the type of data this new node instance will output.
* @return a new Node
*/
public Node create(Node prototype, Class dataClass) {
return create(prototype, null, dataClass);
}
/**
* Create a child node under this node from the given prototype.
*
* @param prototype the prototype node
* @param name the name of the new node
* @param dataClass the type of data this new node instance will output.
* @return a new Node
*/
public Node create(Node prototype, String name, Class dataClass) {
if (prototype == null) throw new IllegalArgumentException("Prototype cannot be null.");
if (dataClass == null) dataClass = prototype.getDataClass();
if (name == null) name = uniqueName(prototype.getName());
Node newNode = prototype.rawInstance(library, name, dataClass);
add(newNode);
return newNode;
}
public boolean remove(Node node) {
assert (node != null);
if (!containsChildNode(node))
return false;
node.markDirty();
node.disconnect();
node.parent = null;
children.remove(node.getName());
if (node == renderedChild) {
setRenderedChild(null);
}
getLibrary().fireChildRemoved(this, node);
return true;
}
public String uniqueName(String prefix) {
Matcher m = NUMBER_AT_THE_END.matcher(prefix);
m.find();
String namePrefix = m.group(1);
String number = m.group(2);
int counter;
if (number.length() > 0) {
counter = Integer.parseInt(number);
} else {
counter = 1;
}
while (true) {
String suggestedName = namePrefix + counter;
if (!containsChildNode(suggestedName)) {
// We don't use rename here, since it assumes the node will be in
// this network.
return suggestedName;
}
++counter;
}
}
public boolean containsChildNode(String nodeName) {
return children.containsKey(nodeName);
}
public boolean containsChildNode(Node node) {
return children.containsValue(node);
}
public boolean containsChildPort(Port port) {
// TODO: This check will need to change once we move to readonly.
return port.getParentNode() == this;
}
public Node getChild(String nodeName) {
return children.get(nodeName);
}
public Node getExportedChild(String nodeName) {
Node child = getChild(nodeName);
if (child == null) return null;
if (child.isExported()) {
return child;
} else {
return null;
}
}
public Node getChildAt(int index) {
Collection c = children.values();
if (index >= c.size()) return null;
return (Node) c.toArray()[index];
}
public int getChildCount() {
return children.size();
}
public boolean hasChildren() {
return !children.isEmpty();
}
public List getChildren() {
return new ArrayList(children.values());
}
//// Rendered ////
public Node getRenderedChild() {
return renderedChild;
}
public void setRenderedChild(Node renderedChild) {
if (renderedChild != null && !containsChildNode(renderedChild)) {
throw new NotFoundException(this, renderedChild.getName(), "Node '" + renderedChild.getAbsolutePath() + "' is not in this network (" + getAbsolutePath() + ")");
}
if (this.renderedChild == renderedChild) return;
this.renderedChild = renderedChild;
markDirty();
getLibrary().fireRenderedChildChanged(this, renderedChild);
}
public boolean isRendered() {
return parent != null && parent.getRenderedChild() == this;
}
public void setRendered() {
if (parent == null) return;
parent.setRenderedChild(this);
}
//// Path ////
public String getAbsolutePath() {
ArrayList parts = new ArrayList();
Node child = this;
Node root = getLibrary().getRootNode();
while (child != null && child != root) {
parts.add(0, child.getName());
child = child.getParent();
}
if (parts.isEmpty()) {
return "/";
} else {
return "/" + StringUtils.join(parts, "/");
}
}
//// Prototype ////
public Node getPrototype() {
return prototype;
}
//// Data Class ////
public Class getDataClass() {
return dataClass;
}
public void validate(Object value) throws IllegalArgumentException {
// Null is accepted as a default value.
if (value == null) return;
if (!getDataClass().isAssignableFrom(value.getClass()))
throw new IllegalArgumentException("Value " + value + " is not of required class (was " + value.getClass() + ", required " + getDataClass());
}
//// Position ////
public double getX() {
return x;
}
public void setX(double x) {
this.x = x;
getLibrary().fireNodeAttributeChanged(this, Attribute.POSITION);
}
public double getY() {
return y;
}
public void setY(double y) {
this.y = y;
getLibrary().fireNodeAttributeChanged(this, Attribute.POSITION);
}
public Point getPosition() {
return new Point((float) x, (float) y);
}
public void setPosition(Point p) {
setPosition(p.getX(), p.getY());
}
public void setPosition(double x, double y) {
if (this.x == x && this.y == y) return;
this.x = x;
this.y = y;
getLibrary().fireNodeAttributeChanged(this, Attribute.POSITION);
}
//// Export flag ////
public boolean isExported() {
return exported;
}
public void setExported(boolean exported) {
this.exported = exported;
getLibrary().fireNodeAttributeChanged(this, Attribute.EXPORT);
}
//// Parameters ////
/**
* Get a list of all parameters for this node.
*
* @return a list of all the parameters for this node.
*/
public List getParameters() {
return new ArrayList(parameters.values());
}
public int getParameterCount() {
return parameters.size();
}
public Parameter addParameter(String name, Parameter.Type type) {
Parameter p = new Parameter(this, name, type);
parameters.put(name, p);
getLibrary().fireNodeAttributeChanged(this, Attribute.PARAMETER);
return p;
}
public Parameter addParameter(String name, Parameter.Type type, Object value) {
Parameter p = addParameter(name, type);
p.setValue(value);
getLibrary().fireNodeAttributeChanged(this, Attribute.PARAMETER);
return p;
}
/**
* Remove a parameter with the given name.
*
* If the parameter does not exist, this method returns false.
*
* @param name the parameter name
* @return true if the parameter exists and was removed.
*/
public boolean removeParameter(String name) {
// First remove all dependencies to and from this parameter.
// Don't rewrite any expressions.
Parameter p = parameters.get(name);
if (p == null) return false;
p.removedEvent();
parameters.remove(name);
getLibrary().fireNodeAttributeChanged(this, Attribute.PARAMETER);
markDirty();
return true;
}
/**
* Get a parameter with the given name
*
* @param name the parameter name
* @return a Parameter or null if the parameter could not be found.
*/
public Parameter getParameter(String name) {
return parameters.get(name);
}
/**
* Checks if this node has a parameter with the given name.
*
* @param name the parameter name
* @return true if a Parameter with that name exists
*/
public boolean hasParameter(String name) {
return getParameter(name) != null;
}
/**
* This method gets called by Parameter.setName().
* At this point, the Parameter already has its new name, but still needs to be stored
* under its new name in parameters.
*
* @param p the parameter to rename.
* @param oldName the old name
* @param newName the new name.
*/
/* package private */ void renameParameter(Parameter p, String oldName, String newName) {
assert (p.getName().equals(newName));
parameters.remove(oldName);
parameters.put(newName, p);
}
//// Parameter values ////
public Object getValue(String parameterName) {
Parameter p = getParameter(parameterName);
if (p == null) return null;
return p.getValue();
}
public int asInt(String parameterName) {
Parameter p = getParameter(parameterName);
if (p.getType() != Parameter.Type.INT) {
throw new RuntimeException("Parameter " + parameterName + " is not an integer.");
}
return p.asInt();
}
public float asFloat(String parameterName) {
Parameter p = getParameter(parameterName);
if (p.getType() != Parameter.Type.FLOAT && p.getType() != Parameter.Type.INT) {
throw new RuntimeException("Parameter " + parameterName + " is not a float.");
}
return p.asFloat();
}
public String asString(String parameterName) {
Parameter p = getParameter(parameterName);
// No type checking is performed here. Any parameter type can be converted to a String.
return p.asString();
}
public Color asColor(String parameterName) {
Parameter p = getParameter(parameterName);
if (p.getType() != Parameter.Type.COLOR) {
throw new RuntimeException("Parameter " + parameterName + " is not a color.");
}
return p.asColor();
}
public NodeCode asCode(String parameterName) {
Parameter p = getParameter(parameterName);
if (p.getType() != Parameter.Type.CODE) {
throw new RuntimeException("Parameter " + parameterName + " is not a string.");
}
return p.asCode();
}
public void setValue(String parameterName, Object value) throws IllegalArgumentException {
Parameter p = parameters.get(parameterName);
if (p == null)
throw new IllegalArgumentException("Parameter " + parameterName + " does not exist.");
p.setValue(value);
}
/**
* Sets a parameter value on this node without raising any errors.
*
* @param parameterName The parameter name.
* @param value The new value.
* @deprecated Will be removed in NodeBox 2.3. Handles should migrate to their own silentSet() method.
*/
public void silentSet(String parameterName, Object value) {
// HACK this method now refers to the current document because otherwise the set will not trigger a network update.
NodeBoxDocument.getCurrentDocument().silentSet(this, parameterName, value);
}
//// Ports ////
public Port addPort(String name) {
return addPort(name, Port.Cardinality.SINGLE);
}
public Port addPort(String name, Port.Cardinality cardinality) {
Port p = new Port(this, name, cardinality);
ports.put(name, p);
// TODO: Test this removal!
// if (parent != null) {
// if (parent.childGraph == null)
// parent.childGraph = new DependencyGraph();
// parent.childGraph.addDependency(p, outputPort);
// }
getLibrary().fireNodeAttributeChanged(this, Attribute.PORT);
return p;
}
public void removePort(String name) {
throw new UnsupportedOperationException("removePort is not implemented yet.");
// TODO: Implement, make sure to remove internal dependencies.
// parent.childGraph.removeDependency(p, outputPort);
}
public Port getPort(String name) {
return ports.get(name);
}
public boolean hasPort(String portName) {
return ports.containsKey(portName);
}
public List getPorts() {
return new ArrayList(ports.values());
}
public Port getOutputPort() {
return outputPort;
}
/**
* Get the value of a port.
*
* This only works for ports with single cardinality.
*
* @param name the name of the port
* @return the value of the port
*/
public Object getPortValue(String name) {
return ports.get(name).getValue();
}
/**
* Get the values of a port as a list of objects.
*
* This only works for ports with multiple cardinality.
*
* @param name the name of the port
* @return the values of the port
*/
public List