Hacking on the Source - NetLogo/Web-Extension GitHub Wiki

This section will walk you through the creation of a typical primitive. If you have not already done so, please read Knowing Your Way Around the Source before proceeding.

First, let's imagine that we wanted to write the "fine" version of the import-world primitive from scratch, using a hook. import-world, of course, comes from vanilla NetLogo, but the basic version can only retrieve the world state from the local filesystem. Instead, we would like to do able to grab a world state file from a remote URL—and customizably so.

As such, we know the following:

  • By nature of being a web primitive that will calls into the HTTP library (and uses a hook to do so), it really should mix in RequesterGenerator.
  • We should also mix in a subclass of WebPrimitive, since we're making a web primitive. Two subclasses exist: WebCommand and WebReporter. import-world is a command, so this should be a WebCommand.
  • We want fine control over the primitive. There are two basic traits for levels of control: SimpleWebPrimitive (handles just a destination argument; not finely-controlling) and CommonWebPrimitive (handles a destination argument, a method argument, and a params argument; finely-controlling). Consequently, we'll want CommonWebPrimitive.

Alright, so here's our code for the primitive's enclosing object declaration:

object ImportWorldFine extends WebCommand with CommonWebPrimitive with RequesterGenerator

RequesterGenerator guides a primitive in how to create a Requester. Requesters send HTTP requests, given parameters. Here, though, we want the Requester to hook into NetLogo with its response and import the world from there. Sure, it's really not all that practical for an importer to use a hook (since the response to the request could just be returned back to report/perform, instead), but this example is presented for academic purposes and simplicity. Generally, you should only have to use hooks for exporting primitives.

Anyway, RequesterGenerator streamlines the creation of specialized Requesters. In doing so, it requires us to implement the RequesterCons type (to tell us what the type signature of the constructor of the Requester will be) and the generateRequester method (to call the aforementioned constructor). In this case our Requester just needs to get an InputStream for the world state file and import it—no return value necessary. That would be InputStream => Unit. Then, generateRequester, as always, will just takes a hook function of type RequesterCons and return a Requester that knows how to handle it. As such, our code for these two implementations will look like this:

override protected type RequesterCons = (InputStream => Unit)
override protected def generateRequester = (hook: InputStream => Unit) => new WorldImporter(hook) with Integration

Integration is defined within RequesterGenerator, and it tells the Requester what kind of system (if any) to configure itself to integrate with. By default, it is set to SimpleWebIntegration (which essentially does nothing special). Ideally, in the future, we'd like to be able to change Integration system-wide based on configuration, so it makes sense to use an Integration type variable everywhere, rather than directly writing out SimpleWebIntegration everywhere.

Next, let's implement the WorldImporter that we alluded to in the previous snippet. It's a Requester with a dynamically-set WebIntegration, as mentioned above. When we want to make a web request with a Requester, we call its apply method, so let's override that with something that makes the request, gets the result as an InputStream and feeds that stream into the hook.

protected class WorldImporter(hook: InputStream => Unit) extends Requester {
  self: WebIntegration =>
  override def apply(dest: String, httpMethod: http.RequestMethod, params: Map[String, String]) : (InputStream, String) = {
    val (responseStream, statusCode) = super.apply(dest, httpMethod, params)
    EventEvaluator(responseStream, hook)
    (responseStream, statusCode)
  }
}

Hooks should pretty much always run in the event thread, so we hand the stream and hook off to EventEvaluator to handle that. Also, you'll note that we return reponseStream and statusCode (because Requester.apply must return an InputStream and a String). Since this is a WebCommand (and not a WebReporter), the data retrieved there will never reach the user in that form, anyway, so, messy as it is, we just can ignore the return that we get from WorldImporter.apply when we call it.

After that, all that remains to be written is the code that ties this all together: ImportWorldFine.perform. In order to do an import-world, we need to get ahold of the workspace's importWorld method, which comes from AbstractWorkspace, meaning that we don't even need to make use of EnsuranceAgent.ensuringGUIWorkspace here. After we import the state, we can create a import-world hook (matching the type of RequesterCons, naturally), make the Requester, and then make the HTTP request through it.

Since this is the a WebCommand—instead of being a DefaultCommand—the signature of the perform method needs to be def perform(args: Array[Argument])(implicit context: Context, ignore: DummyImplicit). The DummyImplicit is there to give def perform(args: Array[Argument])(implicit context: Context) and def perform(args: Array[Argument], context: Context) different type signatures to the JVM. Anyway, the code is as follows:

override def perform(args: Array[Argument])(implicit context: Context, ignore: DummyImplicit) {
  ensuringExtensionContext { (extContext: ExtensionContext) =>
    val workspace = extContext.workspace()
    val hook = (stream: InputStream) => workspace.importWorld(new InputStreamReader(stream))
    val (dest, requestMethod, paramMap) = processArguments(args)
    generateRequester(hook)(dest, requestMethod, paramMap)
  }
}

Alright, awesome. That's it. Here's the full version:

object ImportWorldFine extends WebCommand with CommonWebPrimitive with RequesterGenerator {

  override protected type RequesterCons = (InputStream => Unit)
  override protected def generateRequester = (hook: InputStream => Unit) => new WorldImporter(hook) with Integration

  override def perform(args: Array[Argument])(implicit context: Context, ignore: DummyImplicit) {
    ensuringExtensionContext { (extContext: ExtensionContext) =>
      val workspace = extContext.workspace()
      val hook = (stream: InputStream) => workspace.importWorld(new InputStreamReader(stream))
      val (dest, requestMethod, paramMap) = processArguments(args)
      generateRequester(hook)(dest, requestMethod, paramMap)
    }
  }

  protected class WorldImporter(hook: InputStream => Unit) extends Requester {
    self: WebIntegration =>
    override def apply(dest: String, httpMethod: http.RequestMethod, params: Map[String, String]) : (InputStream, String) = {
      val (responseStream, statusCode) = super.apply(dest, httpMethod, params)
      EventEvaluator(responseStream, hook)
      (responseStream, statusCode)
    }
  }

}