/* Author: Jeff Dalton * Updated: Tue Dec 4 02:33:35 2001 by Jeff Dalton * Copyright: (c) 2001, AIAI, University of Edinburgh */ package ix.ideel; import java.util.*; import ix.ideel.event.*; import ix.icore.domain.*; import ix.icore.domain.event.*; import ix.icore.process.Variable; import ix.icore.*; import ix.util.*; import ix.util.lisp.*; import ix.util.match.*; /** * An externally-driven controller for I_DEEL. */ public class IdeelController implements IssueManager, DomainListener { IXAgent ideel; Domain domain; List controllerListeners = new LinkedList(); LListCollector issues = new LListCollector(); IdeelIssue openOtherReportsIssue = null; // if exists and // status not complete Hashtable handlerTable = new Hashtable(); List universalHandlers = new LListCollector(); public IdeelController(IXAgent ideel) { this.ideel = ideel; } public void reset() { issues.clear(); // Tell viewer? /\/ openOtherReportsIssue = null; } public void setDomain(Domain domain) { this.domain = domain; this.domain.addDomainListener(this); } public void addControllerListener(ControllerListener listener) { controllerListeners.add(listener); } public List getIssues() { return issues; } public synchronized void addIssue(IdeelIssue i) { addIssueOptions(i); issues.addElement(i); fireIssueAdded(i); } public synchronized void addIssue(Issue i) { Debug.assert(!(i instanceof IdeelIssue), "wrong addIssue for", i); addIssue(new IdeelIssue(i)); } public void fireIssueAdded(IdeelIssue issue) { Debug.noteln("fireIssueAdded", issue); ControllerEvent event = new ControllerEvent(this); for (Iterator i = controllerListeners.iterator(); i.hasNext();) { ControllerListener listener = (ControllerListener)i.next(); listener.issueAdded(event, issue); } } void addIssueOptions(IdeelIssue i) { // Standard options that all issues have. i.addOption(new IssueOption.NoActionOption()); i.addOption(new IssueOption.ManualOption()); i.addOption(new IssueOption.NotApplicableOption()); // Add a bind option if there are any unbound variables in // the issue pattern. { Set vars = i.getUnboundVars(); if (!vars.isEmpty()) i.addOption(new BindOption(i)); } // Add options from "universal" handlers for (Iterator u = universalHandlers.iterator(); u.hasNext();) { IdeelIssueHandler h = (IdeelIssueHandler)u.next(); if (h.appliesTo(i)) h.addIssueOptions(i); } // Expansion and manual primitive options from schemas // in the domain. // /\/: Note that we can no longer use getMatchingSchemas(), // because we don't want the schemas to be instantiated. for (Iterator si = domain.getAllSchemas().iterator(); si.hasNext();) { Schema s = (Schema)si.next(); MatchEnv e = SimpleMatcher.match(s.pattern, i.pattern); if (e == null) { continue; } else if (!s.nodes.isNull()) { i.addOption(new ExpandOption(i, s, e)); } else { // When there are no subnodes, the schema name still // specifies a way of doing it manually. i.addOption(new IssueOption.ManualOption(s.name)); } } // Options from I_DEEL's issue handlers. // At present, there is at most one handler for a given issue. IdeelIssueHandler h = getIssueHandler(i); if (h != null && h.appliesTo(i)) { // The handler decides what options to add, but usually // it will be a new AutomaticOption(i, h). h.addIssueOptions(i); } } public synchronized void handleIssue(IdeelIssue i, IssueOption opt) { Debug.assert(issues.contents().find(i), "Stray issue", i); Debug.assert(i.hasOption(opt), "Stray issue option", opt); opt.handleIssue(i); } public synchronized void addIssueHandler(Object verb, IssueHandler h) { // At some point, this should see if any issues should get // an additional option, in case a handler is added after // some issues exist. String key = verb.toString(); if (handlerTable.get(key) != null) Debug.warn("Replacing handler for verb " + Util.quote(key)); handlerTable.put(key, h); } IdeelIssueHandler getIssueHandler(IdeelIssue i) { return (IdeelIssueHandler)handlerTable.get(i.verb.toString()); } public synchronized void addUniversalIssueHandler(IssueHandler h) { universalHandlers.add(h); } public void expandIssue(IdeelIssue i, Object instructions) { // This is really a kind of issue handler. /\/ Schema s = (Schema)instructions; // Avoid null pointer exceptions in debugging code: /\/ if (s.name == null) s.name = "temporary partial schema"; // Note that we don't add the option to the issue. /\/ // Instead we handle "directly". IssueOption opt = new ManualExpandOption(i, s); i.addOption(opt); // ??? but we do add it ??? /\/ opt.handleIssue(i); fireIssueHandled(i, opt); } public void fireIssueHandled(IdeelIssue issue, IssueHandler handler) { Debug.noteln("fireIssueHandled " + issue + " " + handler.getActionDescription()); ControllerEvent event = new ControllerEvent(this); for (Iterator i = controllerListeners.iterator(); i.hasNext();) { ControllerListener listener = (ControllerListener)i.next(); listener.issueHandled(event, issue, handler); } } /* * Var binding */ public void fireNewBindings(Map bindings) { ControllerEvent event = new ControllerEvent(this); for (Iterator i = controllerListeners.iterator(); i.hasNext();) { ControllerListener listener = (ControllerListener)i.next(); listener.newBindings(event, bindings); } } /* * Handle new report */ public synchronized void newReport(Report report) { for (Enumeration e = issues.elements(); e.hasMoreElements();) { IdeelIssue i = (IdeelIssue)e.nextElement(); if (!(i instanceof OtherReportsIssue) && i.wantsReport(report)) { i.addReport(report); return; } } if (openOtherReportsIssue == null) { openOtherReportsIssue = new OtherReportsIssue("Note other reports"); addIssue(openOtherReportsIssue); } openOtherReportsIssue.addReport(report); } public class OtherReportsIssue extends IdeelIssue { public OtherReportsIssue(String text) { super(text); setStatus(STATUS_EXECUTING); } boolean wantsReport(Report report) { Debug.noteln("Other reports issue asked if it wants", report); return status != STATUS_COMPLETE; } boolean actionCanBeTakenNow(IssueOption opt) { return status == STATUS_POSSIBLE || (opt instanceof IssueOption.ManualOption && status == STATUS_EXECUTING); } public void setStatus(int status) { if (status == STATUS_COMPLETE && this == IdeelController.this.openOtherReportsIssue) IdeelController.this.openOtherReportsIssue = null; super.setStatus(status); } void setStatusBasedOn(Report report) { // We don't want to become COMPLETE or IMPOSSIBLE // because of a report, so we override the inherited // method. } } /* * DomainListener methods */ public void schemaAdded(SchemaEvent event) { Schema s = event.getSchema(); Debug.noteln("Controller noting new schema", s); // See if any issues now have a new way to expand. for (Enumeration e = issues.elements(); e.hasMoreElements();) { IdeelIssue i = (IdeelIssue)e.nextElement(); MatchEnv env = SimpleMatcher.match(s.pattern, i.pattern); if (env == null) { continue; } else if (s.nodes.isNull()) { Debug.noteln("New manual primitive for", i); i.addOption(new IssueOption.ManualOption(s.name)); } else { Debug.noteln("New expansion for", i); i.addOption(new ExpandOption(i, s, env)); } } } /* * Issue options */ class BindOption extends IssueOption { IdeelIssue issue; BindingViewer binder; BindOption(IdeelIssue issue) { this.issue = issue; this.shortDescription = "Bind variables"; } public void handleIssue(Issue issue_to_handle) { Debug.assert(issue_to_handle == this.issue); Set unboundVars = issue.getUnboundVars(); if (unboundVars.isEmpty()) throw new RuntimeException("No variables to bind."); binder = new BindingViewer (ideel, unboundVars, new Runnable() { public void run() { newBindings(); } }); } void newBindings() { // Here's where we actually assign the values. Map bindings = binder.newBindings; for (Iterator i = bindings.entrySet().iterator(); i.hasNext();) { Map.Entry e = (Map.Entry)i.next(); Variable var = (Variable)e.getKey(); Object val = e.getValue(); var.setValue(val); } IdeelController.this.fireNewBindings(bindings); } } /** * Handles an issue by expanding it into subissues * as specified by a schema. */ class ExpandOption extends IssueOption { Issue issue; Schema schema; MatchEnv env; ExpandOption(Issue issue, Schema schema, MatchEnv env) { this.issue = issue; this.schema = schema; this.env = env; this.shortDescription = "Expand using " + schema.name; } public void handleIssue(Issue issue) { // That an option's been selecte implies that execution // has begun. /\/: But subnodes must be put in first // or they will get the wrong status as they are added // (because parent will be executing and orderings won't // yet be in so it will look like all preconditions have // been met). do_expansion((IdeelIssue)issue); Debug.assert(issue.getStatus() == STATUS_POSSIBLE); issue.setStatus(STATUS_EXECUTING); } protected void do_expansion(IdeelIssue issue) { Debug.noteln("Expanding using", schema.toLList()); issue.expandOneLevel(schema, env); for (Iterator i = issue.children.iterator(); i.hasNext();) { IdeelIssue child = (IdeelIssue)i.next(); IdeelController.this.addIssue(child); } } } /** * Handles an issue by expanding it into subissues as specified * explicitly by the user. */ class ManualExpandOption extends ExpandOption { ManualExpandOption(Issue issue, Schema schema) { super(issue, schema, new MatchEnv()); this.shortDescription = "Expand as below"; } public void handleIssue(Issue issue) { do_expansion((IdeelIssue)issue); if (issue.getStatus() == STATUS_POSSIBLE) issue.setStatus(STATUS_EXECUTING); } } /** * Handles an issue by invoking one of the agent-level * issue-handlers. */ public static class AutomaticOption extends IssueOption { Issue issue; IssueHandler handler; public AutomaticOption(Issue i, IssueHandler h) { this(i, h, "Do it using " + h.getActionDescription()); } public AutomaticOption(Issue i, IssueHandler h, String descr) { this.issue = i; this.handler = h; this.shortDescription = descr; } public void handleIssue(Issue i) { Debug.noteln("Giving issue to handler", handler); handler.handleIssue(i); // N.B. It is up to that handler to do: // i.setStatus(STATUS_COMPLETE); // or whatever other status changes are required. } } }