package nodebox.node; import nodebox.util.FileUtils; import org.xml.sax.SAXException; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import java.io.*; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Set; /** * A Node library stores a set of (possibly hierarchical) nodes. *

* Node libraries are both documents and libraries. By mentioning a node on the library search path, you can get * all the nodes. *

* Node libraries can be backed by files, and saved in that same file, or they can be stored in memory only. *

* This implementation of the node library only stores a root node. * Calling get(String) on the library actually forwards the call to getChild() on the root node. */ public class NodeLibrary { public static final NodeLibrary BUILTINS = new NodeLibrary(); private String name; private File file; private Node rootNode; private HashMap variables; private NodeCode code; private DependencyGraph parameterGraph = new DependencyGraph(); /** * Load a library from the given XML. *

* This library is not added to the manager. The manager is used only to look up prototypes. * You can add the library to the manager yourself using manager.add(), or by calling * manager.load(). * * @param libraryName the name of the new library * @param xml the xml data of the library * @param manager the manager used to look up node prototypes. * @return a new node library * @throws RuntimeException When the string could not be parsed. * @see nodebox.node.NodeLibraryManager#add(NodeLibrary) * @see nodebox.node.NodeLibraryManager#load(String, String) */ public static NodeLibrary load(String libraryName, String xml, NodeLibraryManager manager) throws RuntimeException { try { NodeLibrary library = new NodeLibrary(libraryName); load(library, new ByteArrayInputStream(xml.getBytes("UTF8")), manager); return library; } catch (ParserConfigurationException e) { throw new RuntimeException("Error in the XML parser configuration", e); } catch (SAXException e) { throw new RuntimeException("Error while parsing: " + e.getMessage(), e); } catch (IOException e) { throw new RuntimeException("I/O error while parsing.", e); } } /** * Load a library from the given file. *

* This library is not added to the manager. The manager is used only to look up prototypes. * You can add the library to the manager yourself using manager.add(), or by calling * manager.load(). * * @param f the file to load * @param manager the manager used to look up node prototypes. * @return a new node library * @throws RuntimeException When the file could not be found, or parsing failed. * @see nodebox.node.NodeLibraryManager#add(NodeLibrary) * @see nodebox.node.NodeLibraryManager#load(File) */ public static NodeLibrary load(File f, NodeLibraryManager manager) throws RuntimeException { try { // The library name is the file name without the ".ndbx" extension. // Chop off the .ndbx String libraryName = FileUtils.stripExtension(f); NodeLibrary library = new NodeLibrary(libraryName, f); load(library, new FileInputStream(f), manager); return library; } catch (ParserConfigurationException e) { throw new RuntimeException("Error in the XML parser configuration", e); } catch (SAXException e) { throw new RuntimeException("Error while parsing: " + e.getMessage(), e); } catch (FileNotFoundException e) { throw new RuntimeException("File not found " + f, e); } catch (IOException e) { throw new RuntimeException("I/O error while parsing " + f, e); } } /** * This method gets called from the public load method and does the actual parsing. *

* The method requires a newly created (empty) library. Nodes are added to this library. * * @param library the newly created library * @param is the input stream data * @param manager the manager used for looking up prototypes. * @throws IOException when the data could not be loaded * @throws ParserConfigurationException when the parser is incorrectly configured * @throws SAXException when the data could not be parsed */ private static void load(NodeLibrary library, InputStream is, NodeLibraryManager manager) throws IOException, ParserConfigurationException, SAXException { SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser parser = spf.newSAXParser(); NDBXHandler handler = new NDBXHandler(library, manager); parser.parse(is, handler); } private NodeLibrary() { this.name = "builtins"; this.file = null; this.rootNode = null; this.variables = null; } public NodeLibrary(String name) { this(name, null); } public NodeLibrary(String name, File file) { this.name = name; this.file = file; this.rootNode = Node.ROOT_NODE.newInstance(this, "root"); this.variables = new HashMap(); } public String getName() { return name; } public File getFile() { return file; } //// Node management //// public Node getRootNode() { return rootNode; } public List getExportedNodes() { List allChildren = rootNode.getChildren(); List exportedChildren = new ArrayList(allChildren.size()); for (Node child : allChildren) { if (child.isExported()) { exportedChildren.add(child); } } return exportedChildren; } public void add(Node node) { if (node.getLibrary() != this) throw new AssertionError("This node is already added to another library."); // The root node can be null in only one case: when we're creating the builtins library. // In that case, the rootNode becomes the given node. if (rootNode == null) { rootNode = node; } else { rootNode.add(node); } } /** * Get a node from this library. *

* Only exported nodes are returned. If you want all nodes, use getRootNode().getChild() * * @param name the name of the node * @return the node, or null if a node with this name could not be found. */ public Node get(String name) { if ("root".equals(name)) return rootNode; return rootNode.getExportedChild(name); } public Node remove(String name) { Node node = rootNode.getChild(name); if (node == null) return null; rootNode.remove(node); return node; } public boolean remove(Node node) { return rootNode.remove(node); } public int size() { return rootNode.size(); } public boolean contains(String nodeName) { return rootNode.contains(nodeName); } //// Variables //// public String[] getVariableNames() { return variables.keySet().toArray(new String[variables.keySet().size()]); } public String getVariable(String name) { return variables.get(name); } public void setVariable(String name, String value) { variables.put(name, value); } //// Code //// public void setCode(NodeCode code) { this.code = code; } public NodeCode getCode() { return code; } //// Persistence ///// public void store() throws IOException, IllegalArgumentException { if (file == null) throw new IllegalArgumentException("Library was not loaded from a file and no file given to store."); store(file); } public void store(File f) throws IOException { NDBXWriter.write(this, f); } /** * Get the full XML data for this library and all of its nodes. * * @return an XML string */ public String toXml() { return NDBXWriter.asString(this); } //// Parameter dependencies //// /** * Add a dependency between two parameters. *

* Whenever the dependent node wants to update, it needs to check if the dependency * is clean. Also, whenever the dependency changes, the dependent gets notified. *

* Do not call this method directly. Instead, let Parameter create the dependencies by using setExpression(). * * @param dependency the parameter that provides the value * @param dependent the parameter that needs the value * @see nodebox.node.Parameter#setExpression(String) */ public void addParameterDependency(Parameter dependency, Parameter dependent) { parameterGraph.addDependency(dependency, dependent); } /** * Remove all dependencies this parameter has. *

* This method gets called when a parameter clears out its expression. *

* Do not call this method directly. Instead, let Parameter remove dependencies by using clearExpression(). * * @param p the parameter * @see nodebox.node.Parameter#clearExpression() */ public void removeParameterDependencies(Parameter p) { parameterGraph.removeDependencies(p); } /** * Remove all dependents this parameter has. *

* This method gets called when the parameter is about to be removed. It signal all of its dependent nodes * that the parameter will no longer be available. *

* Do not call this method directly. Instead, let Parameter remove dependents by using removeParameter(). * * @param p the parameter * @see Node#removeParameter(String) */ public void removeParameterDependents(Parameter p) { parameterGraph.removeDependents(p); } /** * Get all parameters that rely on this parameter. *

* These parameters all have expressions that point to this parameter. Whenever this parameter changes, * they get notified. *

* This list contains all "live" parameters when you call it. Please don't hold on to this list for too long, * since parameters can be added and removed at will. * * @param p the parameter * @return a list of parameters that depend on this parameter. This list can safely be modified. */ public Set getParameterDependents(Parameter p) { return parameterGraph.getDependents(p); } /** * Get all parameters this parameter depends on. *

* This list contains all "live" parameters when you call it. Please don't hold on to this list for too long, * since parameters can be added and removed at will. * * @param p the parameter * @return a list of parameters this parameter depends on. This list can safely be modified. */ public Set getParameterDependencies(Parameter p) { return parameterGraph.getDependencies(p); } //// Standard overrides //// @Override public String toString() { return getName(); } }