package nodebox.node; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import static nodebox.base.Preconditions.checkState; /** * A connectable object on a node. Ports provide input and output capabilities between nodes. *

* Ports have a certain data class. Only ports with the same class of data can be connected together. */ public class Port { public enum Direction { IN, OUT } /** * The cardinality of a port defines if it can store a single value or multiple values. *

* When the cardinality is single, use getValue() and setValue() to access the data. * For ports with multiple cardinality, use getValues(), addValue() and clearValues(). */ public enum Cardinality { SINGLE, MULTIPLE } private Node node; private String name; private Cardinality cardinality; private Direction direction; // Depending on the cardinality, either value or values is used. private Object value; private List values; public Port(Node node, String name) { this(node, name, Cardinality.SINGLE, Direction.IN); } public Port(Node node, String name, Cardinality cardinality) { this(node, name, cardinality, Direction.IN); } public Port(Node node, String name, Direction direction) { this(node, name, Cardinality.SINGLE, direction); } public Port(Node node, String name, Cardinality cardinality, Direction direction) { if (direction == Direction.OUT && cardinality != Cardinality.SINGLE) throw new IllegalArgumentException("Output ports can't have multiple cardinality."); this.node = node; validateName(name); this.name = name; this.cardinality = cardinality; this.direction = direction; } public Node getNode() { return node; } public Node getParentNode() { return node.getParent(); } public boolean hasParentNode() { return getParentNode() != null; } public String getName() { return name; } public void validateName(String name) { if (name == null || name.trim().length() == 0) throw new InvalidNameException(this, name, "Name cannot be null or empty."); if (node.hasPort(name)) throw new InvalidNameException(this, name, "There is already a port named " + name + "."); if (node.hasParameter(name)) throw new InvalidNameException(this, name, "There is already a parameter named " + name + "."); // Use the same validation as for nodes. Node.validateName(name); } public Cardinality getCardinality() { return cardinality; } public Direction getDirection() { return direction; } public void validate(Object value) throws IllegalArgumentException { node.validate(value); } /** * Gets the value of this port. *

* This value will be null if the port is disconnected * or an error occured during processing. * * @return the value for this port. */ public Object getValue() { if (cardinality != Cardinality.SINGLE) throw new AssertionError("You can only call getValue when cardinality is SINGLE."); return value; } /** * Gets a list of values for this port. *

* This method is guaranteed to return a list, although it can be empty. * * @return the values for this port. */ public List getValues() { if (cardinality != Cardinality.MULTIPLE) throw new AssertionError("You can only call getValues when cardinality is MULTIPLE."); if (values == null) return new ArrayList(); return values; } /** * Set the value for this port. *

* This method should not be called directly. Instead, values are set automatically when nodes are updated. *

* This method can only be used when cardinality is set to single. *

* Setting this value will not trigger any notifications or dirty flags. * * @param value the value for this port. * @throws IllegalArgumentException if the value is not of the required data class. */ public void setValue(Object value) throws IllegalArgumentException { if (cardinality != Cardinality.SINGLE) throw new AssertionError("You can only call setValue when cardinality is SINGLE."); validate(value); this.value = value; } /** * Add a value to this port. *

* This method should not be called directly. Instead, values are added automatically when nodes are updated. *

* This method can only be used when cardinality is set to multiple. *

* Adding a value will not trigger any notifications or dirty flags. * * @param value the value to add for this port. * @throws IllegalArgumentException if the value is not of the required data class. */ public void addValue(Object value) throws IllegalArgumentException { if (cardinality != Cardinality.MULTIPLE) throw new AssertionError("You can only call addValue when cardinality is MULTIPLE."); validate(value); if (values == null) values = new ArrayList(); values.add(value); } /** * Reset the value(s) of the port. * This method is called automatically when nodes are updated or disconnected. */ public void reset() { value = null; values = null; } //// Connections //// public boolean isInputPort() { return direction == Direction.IN; } public boolean isOutputPort() { return direction == Direction.OUT; } /** * Checks if this port is connected to another port. * * @return true if this port is connected. */ public boolean isConnected() { checkState(hasParentNode(), "Port %s has no parent node.", this); return getParentNode().isChildConnected(this); } /** * Checks if this port is connected to the given port. * * @param port the other port to check. * @return true if a connection exists between this port and the given port. */ public boolean isConnectedTo(Port port) { if (!isConnected()) return false; if (!hasParentNode()) return false; return getParentNode().isChildConnectedTo(this, port); } /** * Checks if this port is connected to the output port of the given node. * * @param outputNode the node whose output port will be checked. * @return true if a connection exists between this port and the given node. */ public boolean isConnectedTo(Node outputNode) { return isConnectedTo(outputNode.getOutputPort()); } /** * Get the connections on this port. * * @return a list of connections, possibly empty. */ public List getConnections() { Node parent = getParentNode(); if (parent == null) return Collections.emptyList(); List connections = new ArrayList(); for (Connection c : parent.getConnections()) { if (this == c.getInput() || this == c.getOutput()) { connections.add(c); } } return connections; } /** * Checks if this port can connect to the output port of the given node. *

* This method does not check for cyclic dependencies. * * @param outputNode the output (upstream) node. * @return true if the node can be connected. */ public boolean canConnectTo(Node outputNode) { if (outputNode == null) return false; if (getNode() == outputNode) return false; return canConnectTo(outputNode.getOutputPort()); } /** * Check if this port can connect to the given output port. *

* This method does not check for cyclic dependencies. * * @param outputPort the upstream output port. * @return true if this port can connect to the given port. */ public boolean canConnectTo(Port outputPort) { if (outputPort == null) return false; if (outputPort == this) return false; if (outputPort.getDirection() != Direction.OUT) return false; // An input port can only be connected to an output port. // Since we just checked the direction of the output port, // we need to make sure if this port is an input. if (direction != Direction.IN) return false; // Check if the data classes match. // They can either be equal, or the output type can be downcasted to the input type. Class inputClass = node.getDataClass(); Class outputClass = outputPort.node.getDataClass(); return inputClass.isAssignableFrom(outputClass); } /** * Connect this (input) port to the given output node. * * @param outputNode the output node * @return the Connection objects * @throws IllegalArgumentException if the connection could not be made (because of cyclic dependency) * @see Node#connectChildren(Port, Port) */ public Connection connect(Node outputNode) throws IllegalArgumentException { if (outputNode == null) throw new IllegalArgumentException("Output node cannot be null."); if (getParentNode() == null) throw new IllegalArgumentException("This port has no parent node."); return getParentNode().connectChildren(this, outputNode.getOutputPort()); } /** * Disconnects this port. */ public void disconnect() { getParentNode().disconnectChildPort(this); } /** * Create a clone of this port that can be set on the given node. * This new port is not added to the given node. *

* The value of this port is not cloned, since values cannot be cloned. * * @param n the node to clone the port onto. * @return a new Port object */ public Port clone(Node n) { return new Port(n, getName(), getCardinality(), getDirection()); } /** * Copy this port onto the new node. *

* Also clones any upstream connections. * * @param newNode the new node * @return a new, cloned port. */ public Port copy(Node newNode) { return new Port(newNode, getName(), getCardinality(), getDirection()); } /** * Clone the connections of this port onto the new port. *

* If the old connection points to nodes within the copy map they will be replaced with connections to the new node. * * @param newPort the new port to clone the connections onto. * @param copyMap a mapping between the old nodes and the new nodes. */ public void cloneConnection(Port newPort, Map copyMap) { assert newPort.name.equals(getName()); assert newPort.cardinality == cardinality; assert newPort.node != node; checkState(newPort.getConnections().isEmpty(), "The new port should not be connected to anything."); for (Connection c : getConnections()) { Node n = c.getOutputNode(); Node outputNode; if (copyMap.containsKey(n)) { outputNode = copyMap.get(n); } else { outputNode = n; } // If the new node is under a different parent connections cannot be retained, and no // connections are created. if (outputNode.getParent() == newPort.getNode().getParent()) { newPort.connect(outputNode); } } } @Override public String toString() { return node.getName() + "." + getName(); } }