/* File: ProcessModel.java * Contains: The ACP3 process model and workflow stepper * Author: Jeff Dalton * Created: March 1998 * Updated: Fri Jun 15 15:44:44 2001 by Jeff Dalton * Copyright: (c) 1998, 2000, 2001, AIAI, University of Edinburgh */ /* * The code is based in part on an earlier Lisp version by John Levine * and a modification of that Lisp version by Jeff Dalton. */ // If you are reading this file, you should also read the file // ProcessModels/acp3-process.lsp package ix.ileed; import java.util.*; import java.io.*; import ix.util.*; import ix.util.lisp.*; import ix.util.match.*; import ix.icore.process.*; import ix.icore.domain.*; import ix.iface.domain.*; /** * A ProcessModel contains a model of a process and handles the propagation * of state changes. There are a number of interior classes so that the * whole thing can be contained in one file and can avoid name conflicts * with other parts of the panel (e.g. for class names such as "Node").

* * A ProcessModel receives information from a Watcher and is therefore * an implementation of WatcherListener. It sends information to an * implementation of ProcessViewer.

* * Before you can do anything very interesting, you have to install a * viewer and read in a process definition. For example: * *

 *    ProcessModel model = new ProcessModel();
 *    model.setViewer(someViewer);
 *    model.initFromFile("coax-process.lsp");
 * 
* * @see WatcherListener * @see ProcessViewer */ public class ProcessModel implements WatcherListener, StatusValues { // By "action" in the comments below, we mean a String that describes // an action in the process, e.g. "develop COA", or "create support plan". // A "pattern" is a LList (action coaNumber), where coaNumber is a Long. // (For more information, see the javadoc comments for the Node class.) ProcessViewer viewer; // /\/: Separate product viewer Domain domain; // action -> Schema, etc NodeTable nodeTable = new NodeTable(); // pattern -> Node Vector allNodes = new Vector(); // Vector of Nodes Hashtable rowTable = new Hashtable(); // action -> Vector of Nodes Hashtable overallColumn = new Hashtable(); // action -> OverallCell int numberOfCoas = 0; HashedSet changedNodes = new HashedSet(); // set of Nodes Vector products = new Vector(); // process Products Hashtable productTable = new Hashtable(); // for lookup /** * Create an empty ProcessModel. */ public ProcessModel() { } /** * Install a viewer that will be sent status information when changes * occur. */ public synchronized void setViewer(ProcessViewer viewer) { this.viewer = viewer; } /** * Read action definitions from a file. Each action definition (or schema) * specifies the subactions (if any), a partial order over the subactions, * and any special properties of the action that affect how it is modelled. * This information is used when building data structures that represent * the actions that are present in the process being modelled. */ public synchronized void initFromFile(String filename) { try { domain = DomainParser.makeParser(filename).readDomain(); // Make sure there's exactly one process schema. /\/ domain.getProcessSchema(); // Create the process products. for(LList pl = domain.getAllProductSchemas(); pl != Lisp.NIL; pl = pl.cdr()) { ProductSchema ps = (ProductSchema)pl.car(); String pName = (String)ps.name; Debug.noteln("Product", pName); addProduct(new Product(pName)); } } catch (FileNotFoundException e) { Debug.noteException(e); } reset(); } void addProduct(Product p) { Product already = (Product)productTable.get(p.name); Debug.assert(already == null, "Product already exists", p.name); productTable.put(p.name, p); products.addElement(p); } Product getProduct(Object name) { return (Product)productTable.get(name); } public Vector getAllProducts() { return products; } /** * Returns a tree that shows the action nesting of the process, * to the extent that this can be determined directly from the * schema definitions. The tree is a list of entries of the * form (pattern subtree). */ public synchronized LList staticExpansionTree() { return domain.staticExpansionTree(); } /** * Remove all COAs, their associated actions, and all current state * information. However, the definitions established by the most * recent call to initFromFile remain; it is not necessary to call * it again after a reset. */ public synchronized void reset() { Debug.assert(domain != null, "No domain"); Debug.assert(domain.getProcessSchema() != null, "No process schema"); nodeTable.clear(); allNodes.removeAllElements(); rowTable.clear(); overallColumn.clear(); changedNodes.clear(); numberOfCoas = 0; // addCoa(1); for(Enumeration pe = products.elements(); pe.hasMoreElements();) { Product p = (Product)pe.nextElement(); p.status = PRODUCT_BLANK; } } /** * Add the actions for a new COA to the model. Note that the new * COA's number must be exactly one greater than the current number * of COAs. The only way to add a higher-numbered COA is to call * addCoa more than once. */ public synchronized void addCoa(int coaNumber) { Debug.noteln("Adding coa", coaNumber); Debug.assert(coaNumber == numberOfCoas + 1); Long coa = new Long(coaNumber); Schema s = domain.getProcessSchema().forCoa(coa); Node tmpNode = new Node(); // for access to Node methods numberOfCoas++; // Nodes LList children = s.nodes.mapcar(new Function1() { public Object funcall(Object nodeSpec) { LList spec = (LList)nodeSpec; LList childPattern = (LList)spec.elementAt(1); Node child = findNode(childPattern); if (child != null) { // Nodes not per-coa can already exist Debug.assert(childPattern.length() == 1); Debug.assert(!child.schema.isForEachCoa); return child; } // Create a new top-level node with null parent. // This will automatically create all descendents. // The new node and its descendents will register themselves. return ProcessModel.this.new Node(null, childPattern); } }); // Orderings tmpNode.processOrderings(children, s.orderings); // Post-ordering initialization for (LList cl = children; cl != Lisp.NIL; cl = cl.cdr()) { Node child = (Node)cl.car(); child.afterOrderings(); } // Initial nodes may now be ready to execute. for (LList inl = initialNodes(); inl != Lisp.NIL; inl = inl.cdr()) { Node inode = (Node)inl.car(); if (inode.status == STATUS_BLANK) inode.changeStatus(STATUS_POSSIBLE); } // Adding the new nodes will invalidate the overall cells for the // corresponding rows; but it may be that we also need to invalidate // the overall cells that are for rows that have only one node // rather than one per coa. /\/ // Check connectivity checkConnectivity(); } /** * Send the viewer a complete state description, rather than a delta. * This is normally a request from the viewer, for instance if it has * been ignoring changes (for some reason) and wants to catch up. */ public synchronized void sendFullStateDescription() { try { Debug.noteln("Sending full state description"); startRecordingChanges(); // cf tellViewer() ProcessStatusUpdate up = new ProcessStatusUpdate(); up.containsCompleteState = true; up.numberOfCoas = numberOfCoas; up.statusChanges = fullStateDescription(); up.issueCounts = issueCounts(); up.products = products; viewer.statusUpdate(up); resetChangeRecord(); } catch (Exception e) { Debug.noteException(e); } } protected Vector fullStateDescription() { // cf statusChanges() // Get a status description from all nodes Enumeration ce = Seq.map(allNodes.elements(), new Function1() { public Object funcall(Object node) { return ((Node)node).stateRecord(); } }); Vector state = Seq.toVector(ce); // Add in status descriptions from the entire overall column for (ce = overallColumn.elements(); ce.hasMoreElements();) { OverallCell cell = (OverallCell)ce.nextElement(); if (cell.valid == false) { cell.computeStatus(); } state.addElement(cell.stateRecord()); } return state; } /* * * * Methods required by the WatcherListener interface. * */ /** * Called by a watcher when it detects that an action has started. */ public synchronized void startAction(String action) { // We'll use -1 as a convention for "no coa given". startAction(action, -1, -1); } /** * Called by a watcher when it detects that an action has started. */ public synchronized void startAction(String action, int coa) { // We'll use -1 as a convention for "no level given". startAction(action, coa, -1); } /** * Called by a watcher when it detects that an action has started. */ public synchronized void startAction(String action, int coa, int level) { try { Debug.noteln("startAction(" + action + ", " + coa + ", " + level + ")"); startRecordingChanges(); seeIfNewCoa(coa); Node n = coa == -1 ? findActionNode(action) : findActionNode(action, coa); n.actionStarted(level); tellViewer(); resetChangeRecord(); } catch (Exception e) { Debug.noteException(e); } } /** * Called by a watcher when it detects that an action has finished. */ public synchronized void finishAction(String action) { // We'll use -1 as a convention for "no coa given". finishAction(action, -1, -1); } /** * Called by a watcher when it detects that an action has finished. */ public synchronized void finishAction(String action, int coa) { // We'll use -1 as a convention for "no level given". finishAction(action, coa, -1); } /** * Called by a watcher when it detects that an action has finished. */ public synchronized void finishAction(String action, int coa, int level) { try { Debug.noteln("finishAction(" + action + ", " + coa + ", " + level + ")"); startRecordingChanges(); seeIfNewCoa(coa); Node n = coa == -1 ? findActionNode(action) : findActionNode(action, coa); n.actionFinished(level); tellViewer(); resetChangeRecord(); } catch (Exception e) { Debug.noteException(e); } } protected void seeIfNewCoa(int coa) { // /\/: Need to tell the viewer if there are new coas. if (coa == -1) { if (numberOfCoas == 0) addCoa(1); } else { int delta = coa - numberOfCoas; if (delta > 0) { Debug.noteln("Need to make " + delta + " new coa(s)."); while (numberOfCoas < coa) { addCoa(numberOfCoas + 1); } } } } protected void startRecordingChanges() { if (!changedNodes.isEmpty()) { changedNodes.clear(); Debug.warn("starting with nonempty changes"); } } protected void resetChangeRecord() { // Reset for next time. changedNodes.clear(); } protected void tellViewer() { // Tell the viewer. ProcessStatusUpdate up = new ProcessStatusUpdate(); up.numberOfCoas = numberOfCoas; up.statusChanges = statusChanges(); up.issueCounts = issueCounts(); up.products = products; viewer.statusUpdate(up); } protected Vector statusChanges() { // Debug.noteln("Changed nodes:"); // Debug.noteEnumeration(changedNodes.elements()); // Note that we can't just create an enumeration of the changes, // because that's not a snapshot, and we later clear changedNodes. // A Vector makes it easy to add changes from the overall column, // so that's what we used. // Get a list of status changes. Enumeration ce = Seq.map(changedNodes.elements(), new Function1() { public Object funcall(Object node) { return ((Node)node).stateRecord(); } }); Vector changes = Seq.toVector(ce); // Add in changes from the overall column for (ce = overallColumn.elements(); ce.hasMoreElements();) { OverallCell cell = (OverallCell)ce.nextElement(); if (cell.valid == false) { cell.computeStatus(); changes.addElement(cell.stateRecord()); } } return changes; } protected int[] issueCounts() { // We'll sum down the columns. The overall column is number 0. // The issues in common are from nodes that are not one-per-coa. int[] numberOfIssues = new int[numberOfCoas + 1]; int issuesInCommon = 0; int total = 0; Enumeration ne = allNodes.elements(); while (ne.hasMoreElements()) { Node n = (Node)ne.nextElement(); if (n.status == STATUS_EXECUTING || n.status == STATUS_POSSIBLE) { // We call that one issue if (n.coa == StatusChange.COA_NONE) issuesInCommon++; else numberOfIssues[n.coa]++; total++; } } // The issues in common count for every coa, but only once for // the total. /\/: But in some interpretations of what the issues // are, they should count for each COA. int total_again = 0; for (int coa = 1; coa < numberOfIssues.length; coa++) { total_again += numberOfIssues[coa]; numberOfIssues[coa] += issuesInCommon; } Debug.assert(total == total_again + issuesInCommon, "wrong total"); // Issues from the overall column for (Enumeration oe = overallColumn.elements(); oe.hasMoreElements();) { OverallCell c = (OverallCell)oe.nextElement(); if (c.status == STATUS_EXECUTING || c.status == STATUS_POSSIBLE) total++; } numberOfIssues[0] = total; return numberOfIssues; } /* * * * Node code * */ // A node is registered by putting it in nodeTable and in allNodes. // However, it's important to note that registration also ensures // that there is a row (in rowTable) and an overall cell. It turns // out to matter that nodes are registered *before* they're expanded, // because when some overall cells are created, they find a "parent" // cell by looking at a node from their row and then taking the // overall cell of that node's parent node. For that, the parent // node and its overall cell must already be in the data structures. // Therefore these structures have to be created during expansion // "on the way down", not "on the way back up", because some overall // cells look at the structure "above" them at their creation-time. void registerNode(Node node) { Debug.assert(node.pattern != null, "Node registration too soon"); Debug.noteln("Registering Node", node.pattern); Debug.assert(nodeTable.get(node.pattern) == null, "Node aleady exists"); nodeTable.put(node.pattern, node); allNodes.addElement(node); addRowElement(node); } protected Node findNode(LList pattern) { return (Node)nodeTable.get(pattern); } public Node findActionNode(String action) { Node n = findNode(Lisp.list(action)); if (n == null) throw new IllegalArgumentException( "Can't find node for action " + action); return n; } public Node findActionNode(String action, int coa) { Node n = findNode(Lisp.list(action, new Long(coa))); if (n == null) throw new IllegalArgumentException( "Can't find node for action " + action + ", coa " + coa); return n; } /* * * * Matrix rows and the overall column * */ void addRowElement(Node node) { Vector row = (Vector)rowTable.get(node.action); // Make sure the row Vector exists. if (row == null) { Debug.assert(node.coa <= 1); row = new Vector(); rowTable.put(node.action, row); } else { Debug.assert(!row.contains(node)); } // Add the node to the end of the Vector. row.addElement(node); // Invalidate the overall column entry. ensureOverallCell(node.action).invalidate(); } OverallCell ensureOverallCell(String action) { // Make sure the cell exists. OverallCell cell = (OverallCell)overallColumn.get(action); if (cell == null) { cell = newOverallCellInstance(action); overallColumn.put(action, cell); } return cell; } OverallCell findOverallCell(String action) { OverallCell cell = (OverallCell)overallColumn.get(action); Debug.assert(cell != null, "no overall cell"); return cell; } /* * * * Graph utilities * */ // Some of these are used only when testing and are not normally // called at all. // Remember that Node preNode and postNode Vectors contain only // Nodes directly before or after (respectively) at the same "level" // (and so they all have the same parent). public LList topLevelNodes() { Enumeration ne = allNodes.elements(); Enumeration fe = Seq.filter(ne, new Predicate1() { public boolean trueOf(Object a) { Node n = (Node)a; return n.parent == null; } }); return Seq.toLList(fe); // the LList is a snapshot } public LList initialNodes() { return initialNodes(allNodes.elements(), 0); } public LList initialNodes(Enumeration ne, final int level) { Enumeration fe = Seq.filter(ne, new Predicate1() { public boolean trueOf(Object a) { Node n = (Node)a; return n.level == level && n.preNodes.isEmpty(); } }); return Seq.toLList(fe); // the LList is a snapshot } public LList finalNodes() { return finalNodes(allNodes.elements(), 0); } public LList finalNodes(Enumeration ne, final int level) { Enumeration fe = Seq.filter(ne, new Predicate1() { public boolean trueOf(Object a) { Node n = (Node)a; return n.level == level && n.postNodes.isEmpty(); } }); return Seq.toLList(fe); // the LList is a snapshot } public LList nodesBefore(Node after) { return depthFirstFinishOrder(Lisp.list(after), new Function1() { public Object funcall(Object a) { Node n = (Node)a; return n.preNodes.elements(); } }); } public LList nodesAfter(Node before) { return depthFirstFinishOrder(Lisp.list(before), new Function1() { public Object funcall(Object a) { Node n = (Node)a; return n.postNodes.elements(); } }); } public LList nodesBetween(Node before, Node after) { return nodesAfter(before).intersect(nodesBefore(after)); } public LList nodesInDepthFirstFinishOrder() { // LList nodes = Seq.toLList(allNodes.elements()); return depthFirstFinishOrder(initialNodes(), new Function1() { public Object funcall(Object a) { Node n = (Node)a; // The order in succList is just to make the result "nicer" LList postList = Seq.toLList(n.postNodes.elements()); LList succList = n.children.append(postList).reverse(); return succList.elements(); } }); } public LList depthFirstFinishOrder(LList roots, Function1 successors) { Hashtable marks = new Hashtable(); LListCollector result = new LListCollector(); for (LList rs = roots; rs != Lisp.NIL; rs = rs.cdr()) { Object root = rs.car(); dffoWalk_(root, successors, marks, result); } return result.contents(); } private void dffoWalk_(Object at, Function1 sf, Hashtable marks, LListCollector result) { // The separate start and finish marks are only for debugging. if (marks.get(at) == null) { Enumeration successors = (Enumeration)sf.funcall(at); marks.put(at, "start"); while (successors.hasMoreElements()) dffoWalk_(successors.nextElement(), sf, marks, result); result.pushElement(at); // marks.put(at, "finish"); } } /** * Checks that we can reach all of the nodes in various ways. * This method is not called during the normal operation of the * process model but can be called when testing. */ public synchronized void checkConnectivity() { boolean ok = true; Enumeration ne; HashedSet hs; Debug.noteln("Checking connectivity ..."); // See if the descendents of top level nodes reach all nodes. hs = new HashedSet(); for (LList tl = topLevelNodes(); tl != Lisp.NIL; tl = tl.cdr()) { Node n = (Node)tl.car(); hs.addElement(n); Seq.addEnumeration(hs, n.descendents.elements()); } ok = ok & checkNodesReached("Top's descendents", allNodes, hs.elements()); // See if nodesInDepthFirstFinishOrder() contains all nodes. ne = nodesInDepthFirstFinishOrder().elements(); ok = ok & checkNodesReached("DFFO", allNodes, ne); if (!ok) Debug.warn("Connectivity failure"); else Debug.noteln("... finished connectivity check."); } private boolean checkNodesReached(String test, Vector all, Enumeration reached) { boolean ok = true; Mark mAll = new Mark("checkNodesReached all nodes"); Mark mReached = new Mark("checkNodesReached reached nodes"); // Mark all nodes Enumeration a = all.elements(); while (a.hasMoreElements()) ((Node)a.nextElement()).setMark(mAll); // Go through reached nodes while (reached.hasMoreElements()) { Node n = (Node)reached.nextElement(); // Check that it isn't reached twice and is one of "all nodes". if (n.hasMark(mReached)) { Debug.noteln(test + " twice reached", n.pattern); ok = false; } else if (!n.hasMark(mAll)) { Debug.noteln(test + " reached a disconnected", n.pattern); ok = false; } // Mark it as reached n.setMark(mReached); } // Check that all nodes were reached. a = all.elements(); while (a.hasMoreElements()) { Node n = (Node)a.nextElement(); if (!n.hasMark(mReached)) { Debug.noteln(test + " didn't reach", n.pattern); ok = false; } } return ok; } /* * * * Classes in the process model * */ /** * Create an instance of an appropriate OverallCell class. */ OverallCell newOverallCellInstance(String action) { // factory Schema s = domain.getActionSchema(action); Debug.assert(s != null, "Can't find schema for overall"); if (s.getProperty("overall-collects") != null) return this.new OverallCollectionCell(action); else return this.new OverallCell(action); } /** * Overall column cells, a member class of ProcessModel. An OverallCell * summarizes a row in a 2-D tabular (or matrix) description of the * process. All entries on a row are for the same action, but each * can be in a different COA. ("Can be", because some actions appear * only once no matter how many COAs there are. Some rows therefore * have only one entry, but if the matrix were displayed, that entry * would probably be duplicated in every column.) Each column represents * a COA. There is also an overall column that contains OverallCells.

* * The ProcessModel method newOverallCellInstance(String action) is * a factory method that should be used to create an instance of the * appropriate OverallCell class for a action-name (= row).

* * Instances of OverallCell are "standard" cells that compute a status * that is the "least" status of any Node in their row, where "less" * means roughly "would occur earlier in execution". So, for instance, * EXECUTING is less-than COMPLETE. Subclasses of OverallCell may * implement different rules. */ class OverallCell implements StatusValues { String action; int status = STATUS_BLANK; boolean valid = false; String text = null; OverallCell(String action) { this.action = action; } void invalidate() { valid = false; } // Override computeStatus() in a subclass if you want a different // way of determining the status. void computeStatus() { status = statusRule(); valid = true; } // Override statusRule in a subclass if you just want a different rule. protected int statusRule() { Vector row = (Vector)ProcessModel.this.rowTable.get(action); Debug.assert(row != null, "row doesn't exist for overall cell"); int result = STATUS_BLANK; Enumeration re = row.elements(); while (re.hasMoreElements()) { Node n = (Node)re.nextElement(); // It just so happens that higher-numbered status values // should win... (except that BLANK trumps them). if (n.status == STATUS_BLANK) { result = STATUS_BLANK; break; } else if (n.status > result) result = n.status; } // But N/A becomes BLANK overall if (result == STATUS_NA) result = STATUS_BLANK; return result; } StatusChange stateRecord() { StatusChange c = new StatusChange(); c.action = action; c.coa = StatusChange.COA_OVERALL; c.iteration = 0; c.status = status; return c; } } /** * A subclass of OverallCell that reports the collective status of * some earlier row. For instance, it may list all COAs that have * completed the action "develop COA". */ class OverallCollectionCell extends OverallCell implements SchemaSymbols { String collects = null; // from schema overall-collects boolean requireAll = false; // from schema ovarall-at-least int requiredCollectionSize = 0; // from schema ovarall-at-least OverallCell parentCell = null; // set by the constructor(s) OverallCollectionCell(String action) { super(action); Debug.noteln("New overall collection for", action); setCollectionProperties(); parentCell = getParentCell(); } protected void setCollectionProperties() { // Sets collects, requireAll, and requiredCollectionSize. Schema s = ProcessModel.this.domain.getActionSchema(action); collects = s.getProperty("overall-collects"); if (collects != null) { Debug.assert(ProcessModel.this.domain.getActionSchema(collects) != null, "Unknown action to collect"); Object value = s.getPropertyObject("overall-at-least"); if (value == K_ALL) requireAll = true; else if (value != null) { Debug.assert(value instanceof Number); requiredCollectionSize = ((Number)value).intValue(); } } } OverallCell getParentCell() { // This indirectly requires that nodes register themselves // before recursively expanding. Vector row = (Vector)ProcessModel.this.rowTable.get(action); Node anyNode = (Node)row.firstElement(); if (anyNode.parent == null) { // Debug.warn("No parent cell for \"" + action + "\""); return null; } else return ProcessModel.this.findOverallCell( anyNode.parent.action); } void computeStatus() { status = statusRule(); // Leave invalid so the status will always be recomputed, // because we don't handle invalidation for this case. // valid = true; } protected int statusRule() { // N.B. Side-effects the text and requiredCollectionSize fields. Vector row = (Vector)ProcessModel.this.rowTable.get(collects); Debug.assert(row != null, "collected row doesn't exist"); if (requireAll) requiredCollectionSize = ProcessModel.this.numberOfCoas; // Look at the row we're "collecting" to count the number // of nodes on that row that have status COMPLETE. If // numberComplete >= requiredCollectionSize, then we can // have status POSSIBLE and can have the text field list // the COAs that have completed the required action. int numberComplete = 0; Enumeration re = row.elements(); while (re.hasMoreElements()) { Node n = (Node)re.nextElement(); Debug.assert(n.coa >= 1); // ensure this earlier /\/ if (n.status == STATUS_COMPLETE) { numberComplete++; } } // Set the text field. text = null; if (numberComplete >= requiredCollectionSize && !requireAll) { for (re = row.elements(); re.hasMoreElements();) { Node n = (Node)re.nextElement(); if (n.status == STATUS_COMPLETE) { if (text == null) text = "COA-" + String.valueOf(n.coa); else text += "," + String.valueOf(n.coa); } } } // Determine the status // If the parent cell has status COMPLETE, so do we. if (parentCell != null) { int parentStatus = parentCell.valid ? parentCell.status : parentCell.statusRule(); if (parentStatus == STATUS_COMPLETE) return STATUS_COMPLETE; } // Otherwise, it depends on how many COAs have completed // the required action. if (numberComplete >= requiredCollectionSize) return STATUS_POSSIBLE; else return STATUS_BLANK; } StatusChange stateRecord() { StatusChange result = super.stateRecord(); result.text = text; return result; } } /** * Nodes are stored in a NodeTable, indexed by pattern. * * @see ProcessModel#registerNode */ static class NodeTable extends Hashtable { } /** * Nodes represent actions in the process. Each node is based on a * schema. If the node is for a particular COA, the schema must * be instantiated for that coa by calling *

     *    schema.forCoa(coaNumber)
     * 
* where coaNumber is a Long. (It's a Long, because that's how * integer values are represented by the Lisp tokenizer, and hence * in the patterns that appear in Schemas.)

* * Note that Node is a member class of the ProcessModel class, and * so each Node is associated with a particular "containing" instance * of ProcessModel. To make it clearer when Node methods are referring * to fields or methods of the containing instance, we use the syntax * that makes that explicit: *

     *    ProcessModel.this.field
     *    ProcessModel.this.method(...)
     * 
* We also explicitly specify the containing model when creating a new * Node. */ class Node implements Markable, StatusValues, EffectSymbols { Schema schema = null; // the schema LList pattern = null; // e.g. ("review COA" 3), as in the schema String action; // from the pattern int coa; // from the pattern or else COA_NONE // /\/: Need a better name than "level" because "level" // is often a local var. int level = 0; // expansion level, 1 + the parent's int status = STATUS_BLANK; int iteration = 0; // number of times this action has executed int planLevel = -1; // plan level reached by the monitored process boolean isOrJoin = false; boolean isDecisionSplit = false; boolean isLoopEntry = false; boolean setsParentPlanLevel = false; // depends on action String boolean isLoopBack = false; // loopBacks.isEmpty() + a patch /\/ Node parent = null; LList children = Lisp.NIL; LList descendents = Lisp.NIL; Vector preNodes = new Vector(); // nodes directly before this Vector postNodes = new Vector(); // nodes directly after this Vector loopBacks = new Vector(); // nodes this loops back to Vector loopFroms = new Vector(); // nodes that loop back to this LList loopNodes = Lisp.NIL; // only when isLoopEntry Mark mark = null; Node () { // Only for temp node used when adding a coa. /\/ } Node(Node parent, LList pattern) { // Debug.noteln("New node:", pattern); this.parent = parent; this.pattern = pattern; this.schema = getSchema(pattern); this.action = (String)pattern.car(); this.coa = pattern.cdr() != Lisp.NIL ? ((Number)pattern.elementAt(1)).intValue() : StatusChange.COA_NONE; if (parent != null) level = parent.level + 1; this.isOrJoin = this.schema.isTrue("or-join"); this.isDecisionSplit = this.schema.isTrue("decision-split"); this.isLoopEntry = this.schema.isTrue("loop-entry"); this.setsParentPlanLevel = (action.endsWith("to one more level") || action.endsWith("plan-cycle")); registerSelf(); // must be before expansion String statusName = this.schema.getProperty("initial-status"); if (statusName != null) { int statusValue = ViewColor.statusValue(statusName); Debug.assert(statusValue > 0); changeStatus(statusValue); // this must be registered 1st } expand(); } protected void registerSelf() { ProcessModel.this.registerNode(this); } protected Schema getSchema (LList pattern) { String action = (String)pattern.car(); Schema s = ProcessModel.this.domain.getActionSchema(action); Debug.assert(s != null, "Can't find schema"); if (s.isForEachCoa) { return s.forCoa(patternCoa(pattern)); // instantiate schema } else { Debug.assert(pattern.length() == 1, "bogus pattern"); return s; } } protected Long patternCoa(LList pattern) { Debug.assert(pattern.length() == 2, "No coa in pattern"); return (Long)pattern.elementAt(1); } void expand() { final ProcessModel ourModel = ProcessModel.this; // Create the child nodes specified by the schema. // A nodeSpec is a LList (Long:number LList:pattern). children = schema.nodes.mapcar(new Function1() { public Object funcall(Object nodeSpec) { LList spec = (LList)nodeSpec; LList childPattern = (LList)spec.elementAt(1); return ourModel.new Node(Node.this, childPattern); } }); // Add the orderings processOrderings(children, schema.orderings); // If the children were expanded here, rather as they're // constructed, the overall process might be easier to // follow. We'd see a level of nodes created, and then // the orderings added for that level, before recursion // to the next (lower) level of nodes. Right now, instead, // the Node constructor calls expand(), and so when expand() // creates a child, the entire subtree under it is created, // before any orderings are added; and then orderings // are added level-by-level as the recursive calls return. // If this changes, remember to change ProcessModel#addCoa. for (LList cl = children; cl != Lisp.NIL; cl = cl.cdr()) { Node child = (Node)cl.car(); // child.expand(); child.afterOrderings(); } // Collect all descendents descendents = getDescendents(); } void processOrderings(LList nodes, LList orderings) { for (Enumeration ords = orderings.elements(); ords.hasMoreElements();) { LList ord = (LList)ords.nextElement(); int n1 = ((Number)ord.car()).intValue() - 1; int n2 = ((Number)ord.cdr().car()).intValue() -1; Node before = (Node)nodes.elementAt(n1); Node after = (Node)nodes.elementAt(n2); if (n2 > n1) before.linkBefore(after); else before.loopBackTo(after); } } void linkBefore(Node after) { // Debug.noteln(" " + this + "\n --> " + after); postNodes.addElement(after); after.preNodes.addElement(this); // Since after has a new preNode, its status may change. if (after.status == STATUS_POSSIBLE & !after.isReady()) after.changeStatus(STATUS_BLANK); } void loopBackTo(Node after) { // Debug.noteln("Loopback " + this + "\n --> " + after); Debug.assert(after.isLoopEntry, "loop back to non-LoopEntry"); loopBacks.addElement(after); after.loopFroms.addElement(this); if (loopBacks.size() != 1 || after.loopFroms.size() != 1) Debug.warn("Complex loop involving " + this + " and " + after); } void afterOrderings() { // Here we do any initialization that can't happen until // after orderings have been added. Remember that for // nodes that are not 1-per-coa, new orderings can be // added when there's a new coa. if (isLoopEntry) { // Debug.noteln("Nodes that loop back to", this); // Debug.noteEnumeration(loopFroms.elements()); if (loopFroms.size() != 1) { Debug.warn("More than one node loops back to " + this); } else { loopNodes = getLoopNodes(); // Debug.noteln("Loop contents for", this); // Debug.noteEnumeration(loopNodes.elements()); } } if (setsParentPlanLevel) { LList inodes = initialNodes(parent.children.elements(), level); // It's not necessary that sPPL nodes be initial, // but we should probably think about any that aren't. /\/ Debug.assert(parent != null, "sPPL node has parent"); // Debug.assert(isLoopEntry, "sPPL node is loop-entry"); Debug.assert(preNodes.isEmpty(), "sPPL node is initial"); Debug.assert(inodes.length()==1, "sPPL node is only initial"); Debug.assert(inodes.car()==this, "sPPL node found initial"); } // isLoopBack is a faster test, but the proximate reason is because // there are problems in the acp3-process.lsp model so that we've // left out the actual link back from "reject..." nodes. /\/ isLoopBack = !loopBacks.isEmpty() || schema.isTrue("treat as loopback"); // gak! /\/ } LList getLoopNodes() { Node loopEnd = (Node)loopFroms.firstElement(); return ProcessModel.this.nodesBetween(this, loopEnd); } LList getDescendents() { LListCollector d = new LListCollector(); walkDescendents(d.elementAdder()); return d.contents(); } void walkDescendents(Function1 f) { for (LList cl = children; cl != Lisp.NIL; cl = cl.cdr()) { Node child = (Node)cl.car(); f.funcall(child); child.walkDescendents(f); } } // actionStarted(int level) and actionFinished(int level) are // called when the ProcessMonitor receives something from the // watcher, and they do all the interesting work. // The "level" (planLevel) is the plan level being developed. // This level starts at 0, and Nodes initially have iteration == 0. // But when we find out that an action has started, we increment // the iteration count; so the iteration count is normally level + 1. // Since it's level that the viewers display, they're given // iteration - 1 when the number is based on the iteration count. // Note that the Node field "level" is something different -- // expansion depth in the model (ie, how many Node.parent links // you can follow upwards). There are separate Node fields // "level", "planLevel", and "iteration". // /\/: expansionLevel and planningLevel might be better names. void actionStarted(int level) { // We've discovered that this node's action has started. // Since this is called only when we receive an event from // the watcher, we know it has definitely happened. // So the parent action must have started too, and all // actions that must complete before this one can start // must have finished. But it's possible we already // handled those things when processing earlier events. // So if the current status of these other nodes is already // what it should be, we leave them alone. // The level is used only as a check. Perhaps it should // override the iteration count instead. /\/ if (level >= 0 && level != iteration) Debug.noteln("Started " + this + " iteration " + iteration + " but level " + level); actionStarted(); } void actionFinished(int level) { // We've discovered that this node's action has finished. // Since this is called only when we receive an event from // the watcher, we know it has definitely happened. // The level is used only as a check. /\/ if (level >= 0 && level != iteration) Debug.noteln("Finished " + this + " iteration " + iteration + " but level " + level); actionFinished(); } void actionStarted() { // We've decided this node's action has started. // In some cases, we report a level to the parent Node. for (Node n = this; n.parent != null; n = n.parent) { if (n.setsParentPlanLevel) n.parent.setPlanLevel(iteration); else break; } // If the status is already EXECUTING or COMPLETE, then this // is a new iteration of a loop. if (status == STATUS_EXECUTING || status == STATUS_COMPLETE) { if (isLoopEntry) { newLoopIteration(); return; } else if (parent.isLoopEntry) { // /\/: Should allow the parent to be anywhere in a loop. iteration++; changeStatus(STATUS_EXECUTING); parent.newLoopIteration(); return; } else Debug.warn("Looping to non-loop-entry " + this); } // From here on, we can assume we're not yet in a loop // (though a loop back might still happen later on). if (status != STATUS_BLANK && status != STATUS_POSSIBLE) { Debug.warn("Action start for " + this + " with status " + statusName(status)); } iteration++; changeStatus(STATUS_EXECUTING); // Figure out what must have happened at nodes above and before propagateActionStarted(); // Children without predecessors should be at least ready // to exectute. (In fact, they may have already started, // or even finished, because we may be working up from one // of them.) for (LList cl = children; cl != Lisp.NIL; cl = cl.cdr()) { Node child = (Node)cl.car(); if (child.preNodes.isEmpty() && child.status != STATUS_EXECUTING && child.status != STATUS_COMPLETE) child.changeStatus(STATUS_POSSIBLE); } } void newLoopIteration() { // For now, this is the only point at which we try to handle // a new iteration for a node that has already started or // completed execution. Debug.assert(isLoopEntry); Debug.assert(loopNodes.length() > 0); // First, all nodes after this in the loop, and their descendents, // must have completed the previous iteration. We make the // changes directly, without any propagation. for (LList ll = loopNodes; ll != Lisp.NIL; ll = ll.cdr()) { Node n = (Node)ll.car(); if (n == this) continue; if (n.iteration < iteration) { // This node hasn't noticed the current iteration if (n.iteration < iteration-1) Debug.noteln(n + " has fallen behind"); n.iteration = iteration; n.changeStatus(STATUS_COMPLETE); // Update the descendents too. n.completeDescendentExecution(iteration); } } // The descendents of this node must also have completed // the previous iteration. completeDescendentExecution(iteration); // Now this node (the loop-entry) starts the new iteration. iteration++; changeStatus(STATUS_EXECUTING); } void completeDescendentExecution(int minIteration) { // A node has executed; therefore all the nodes under it // must have executed at least once in such a way that // they exited any loops. So we assume the loopBack // nodes didn't execute. /\/: Really it should be all nodes // on loopBack *paths*, but fortunately we don't have any // of those of length > 1. We also assume there aren't any // xor-parallel paths only one of which can be followed. // An alternative would be to make all descendents // complete (and invalidate their iterations counts?), // which is roughly the view we take in actionFinished(). for (LList dl = descendents; dl != Lisp.NIL; dl = dl.cdr()) { Node d = (Node)dl.car(); if (!d.isLoopBack) { if (d.iteration < minIteration) { d.iteration = minIteration; d.changeStatus(STATUS_COMPLETE); } } } } void propagateActionStarted() { // The parent action must have started too, but maybe // we've already handled that. if (parent != null && (parent.status == STATUS_BLANK || parent.status == STATUS_POSSIBLE)) { parent.actionStarted(); } // The actions that had to complete before this one could // start must have completed. But which actions are those? // If this is an and-join, it's all the nodes directly before // this one. If this node is an or-join, we can't really tell. // In any case, if the prerequisites already have status // COMPLETE, we leave them alone. for (Enumeration pre = preNodes.elements(); pre.hasMoreElements();) { Node p = (Node)pre.nextElement(); if (p.status != STATUS_COMPLETE) { if (this.isOrJoin) { // Not clear what to do Debug.noteln("Can't tell which actions before " + pattern + " must have completed."); break; } p.actionFinished(); } } // If one of the nodes directly before this one is a // decision-split, then, since this node has started // executing, all other direct successors of the d-split // cannot execute. We should probably do something about // this. /\/ } void actionFinished() { boolean isNewIteration = false; // We've decided that this node's action has finished. // If its status is EXECUTING, we'll assume this is the // completion of the same iteration. If the status is // BLANK, we'll assume the start wasn't monitored. // If the status is already COMPLETE, then either we // mistakenly inferred completion or we're in some kind // of loop. We don't yet have a way to distinguish the // already-complete cases or to handle properly a loop // detected here. if (status == STATUS_BLANK) { isNewIteration = true; iteration++; } else if (status == STATUS_EXECUTING) { isNewIteration = false; } else if (status == STATUS_COMPLETE) { // Let's try this ... /\/ Debug.noteln("Complete when already complete", this); isNewIteration = true; iteration++; } // The status is now COMPLETE. changeStatus(STATUS_COMPLETE); // The entire tree under this node must be complete too. // Since there can be no order-links with nodes outside // the subtree, we can just change the status of all the // subtree nodes without worrying about propagation. // /\/: It's not clear that this is really right, because // there may be decision-splits in the subtree. Only one // branch can have been taken, so only one can be COMPLETE. // There may also be or-paths in parallel, only some of // which have executed. cf completeDescendentExecution(). for (LList dl = descendents; dl != Lisp.NIL; dl = dl.cdr()) { Node d = (Node)dl.car(); if (isNewIteration) d.iteration++; d.changeStatus(STATUS_COMPLETE); } // Everything that has to happen before the action can // even start must have happened. (See actionStarted.) propagateActionStarted(); // Moreover, some nodes after this one may now be ready // to execute. for (Enumeration post = postNodes.elements(); post.hasMoreElements();) { Node p = (Node)post.nextElement(); if (p.status != STATUS_EXECUTING && p.status != STATUS_COMPLETE && p.isReady()) p.changeStatus(STATUS_POSSIBLE); } } boolean isReady() { // Does not handle the initial-node case. (An initial node has // no preNodes and is ready iff its parent has started execution.) if (this.isOrJoin) { for (Enumeration pre = preNodes.elements(); pre.hasMoreElements();) { Node p = (Node)pre.nextElement(); if (p.status == STATUS_COMPLETE) return true; } return false; } else { // An and-join for (Enumeration pre = preNodes.elements(); pre.hasMoreElements();) { Node p = (Node)pre.nextElement(); if (p.status != STATUS_COMPLETE && p.status != STATUS_NA) return false; } return true; } } void changeStatus(int newStatus) { // Always record the change even if the status will not actually // be different. This is because the iteration count may have // changed. /\/ if (status == STATUS_NA) return; // Debug.noteln("Status " + this + // " --> " + statusName(newStatus)); ProcessModel.this.changedNodes.addElement(this); status = newStatus; findOverallCell(action).invalidate(); // Handle effects here, because it's the only place // that sees all status changes. /\/ handleEffects(schema.effects, newStatus); } void setPlanLevel(int newLevel) { ProcessModel.this.changedNodes.addElement(this); planLevel = newLevel; } String statusName(int status) { String[] name = {"BLANK", "COMPLETE", "EXECUTING", "POSSIBLE", "IMPOSSIBLE", "N/A"}; return name[status]; } StatusChange stateRecord() { StatusChange c = new StatusChange(); c.action = action; c.coa = coa; c.iteration = iteration; c.status = status; c.planLevel = planLevel; return c; } void handleEffects(LList effects, int nodeStatus) { for(LList el = effects; el != Lisp.NIL; el = el.cdr()) { handleEffect((Effect)el.car(), nodeStatus); } } void handleEffect(Effect e, int nodeStatus) { if (nodeStatus == STATUS_EXECUTING & e.when == S_START) { handleEffect(e); } else if (nodeStatus == STATUS_COMPLETE & e.when == S_FINISH) { handleEffect(e); } } // Here's where we finally do what the Effect says to do. /* static */ Object effectPatSyntax = Lisp.readFromString( "(status (?name))" ); /* static */ Symbol Q_NAME = Symbol.intern("?name"); void handleEffect(Effect e) { MatchEnv env = SimpleMatcher.mustMatch(effectPatSyntax, e.pattern); Object name = env.get(Q_NAME); Product p = ProcessModel.this.getProduct(name); Debug.assert(p != null, "No product named", name); Debug.noteln("Changing status of product " + p.name + " to " + e.value); // Change the Product's status if (e.value == K_COMPLETE) p.status = PRODUCT_COMPLETE; else if (e.value == K_DRAFT) p.status = PRODUCT_DRAFT; else Debug.warn("Can't handle effect " + e); } // /\/: A more spohisticated marking mechanism is needed. public void setMark(Mark m) { this.mark = m; } public boolean hasMark(Mark m) { return mark == m; } public String toString(){ return "#"; } } }