Planes

In a previous tutorial, we began to describe a distributed object model where Web Agents are the objects and lanes are fields. Swim planes can, loosely, be seen as a shared context for a group of Web Agents, somewhat analogous to scopes but with more runtime responsibilities.

More specifically, every plane:

Declaration

Simply create a custom class that extends swim.api.plane.AbstractPlane to declare a plane:

// swim/basic/BasicPlane.java
package swim.basic;

import swim.api.plane.AbstractPlane;

public class BasicPlane extends AbstractPlane {
}

However, a typical pattern with Swim planes is to additionally solve the routing responsibility within the declaration. Much like lanes in a Web Agent are annotated with @SwimLane, each Agent type in a plane should be annotated with @SwimRoute. The argument inside the annotation defines a URI pattern (colons (:) indicate dynamic components). Requests that match this pattern are routed to an Agent of the provided type, with instations happening as necessary:

// swim/basic/BasicPlane.java
package swim.basic;

import swim.api.agent.AgentType;
import swim.api.SwimRoute;
import swim.api.plane.AbstractPlane;

public class BasicPlane extends AbstractPlane {
  @SwimRoute("/unit/:id")
  final AgentType<UnitAgent> unitAgentType = agentClass(UnitAgent.class);
}

Instantiation

Somewhat similarly to Web Agents, planes are not typically instantiated using their constructors. Planes don't make much sense outside a Swim server, so we will piggyback off server initialization itself to instantiate planes.

Recall that Swim servers can be loaded from a configuration file. Take note of how we define which plane this server will load:

# /server.recon
@server {
  @plane("basic") {
    class: "swim.basic.BasicPlane"
  }
  @http(port: 9001) {
    plane: "basic"
  }
}

Swim provides a utility class, swim.loader.ServerLoader, to facilitate the actual load step:

// swim/basic/BasicPlane.java
package swim.basic;

import swim.api.agent.AgentType;
import swim.api.SwimRoute;
import swim.api.plane.AbstractPlane;
import swim.api.server.ServerContext;
import swim.loader.ServerLoader;

public class BasicPlane extends AbstractPlane {
  @SwimRoute("/unit/:id")
  final AgentType<UnitAgent> unitAgentType = agentClass(UnitAgent.class);

  // main() can be in any class, we picked this for convenience
  public static void main(String[] args) {
    final ServerContext server = ServerLoader.load(BasicPlane.class.getModule()).serverContext();
    server.start();
    server.run();
  }
}

Lifecycle Callbacks

Just like Web Agents and lanes, planes come with callback functions that are executed during various stages of their lifecycle, and the callbacks are overridable with custom logic. The two that you are most likely to care about are didStart() and willStop(), which work similarly to their Web Agent counterparts:

// swim/basic/BasicPlane.java
package swim.basic;

import swim.api.agent.AgentType;
import swim.api.SwimRoute;
import swim.api.plane.AbstractPlane;
import swim.api.server.ServerContext;
import swim.structure.Value;

public class BasicPlane extends AbstractPlane {
  @SwimRoute("/unit/:id")
  final AgentType<UnitAgent> unitAgentType = agentClass(UnitAgent.class);

  @Override
  public void didStart() {
    command("/unit/master", "WAKEUP", Value.absent());
  }

  @Override
  public void willStop() {
    System.out.println("Shutdown in progress...");
  }

  public static void main(String[] args) {
    final ServerContext server = ServerLoader.load(BasicPlane.class.getModule()).serverContext();
    server.start();
    server.run();
  }
}

Planes Are Swim Handles

In the previous snippet, we called command() from directly within our custom plane class. By themselves being Swim handles, planes have access to the Swim API.

Recall that a Swim server runs asynchronously relative to the main thread that started it, implying that the main thread is free to do work. We demonstrate here how one could use a plane (context) as a Swim handle. Note especially how a simple one-argument difference enables users to go through the network stack and talk to a remote process (we resolve to the same one here to avoid forcing you to start a new process):

// swim/basic/BasicPlane.java
package swim.basic;

import swim.api.agent.AgentType;
import swim.api.SwimRoute;
import swim.api.plane.AbstractPlane;
import swim.api.server.ServerContext;
import swim.structure.Value;

public class BasicPlane extends AbstractPlane {
  @SwimRoute("/unit/:id")
  final AgentType<UnitAgent> unitAgentType = agentClass(UnitAgent.class);

  @Override
  public void didStart() {
    command("/unit/master", "WAKEUP", Value.absent());
  }

  @Override
  public void willStop() {
    System.out.println("Shutdown in progress...");
  }

  public static void main(String[] args) {
    final ServerContext server = ServerLoader.load(BasicPlane.class.getModule()).serverContext();
    server.start();
    final PlaneContext plane = server.getPlane("basic").planeContext();
    server.run();
    // observe the effects of our commands
    plane.downlinkValue()
      .nodeUri("/unit/master")
      .laneUri("info")
      .didSet((newValue, oldValue) -> {
        System.out.println("observed info change to " + newValue + " from " + oldValue);
      })
      .open();
    plane.command("/unit/master", "publishInfo", Text.from("Without network"));
    plane.command("warp://localhost:9001", "/unit/master", "publishInfo", Text.from("With network, no token"));
    plane.command("warp://localhost:9001?token=abcd", "/unit/master", "publishInfo", Text.from("With network, no token"));
  }
}

Security Policies

Security policies can be defined to control client access to a plane. Simply define a custom policy, then inject it into your custom plane upon its initialization.

We will go into more detail about security policies in a later section. In the meantime, the snippet is how one would enhance our custom plane to only accept requests containing the right token as a URL parameter. While not a best practice, it demonstrates the general pattern for injecting policies into planes:

// swim/basic/BasicPlane.java
package swim.basic;

import swim.api.agent.AgentType;
import swim.api.auth.Identity;
import swim.api.SwimRoute;
import swim.api.plane.AbstractPlane;
import swim.api.policy.AbstractPolicy;
import swim.api.policy.PolicyDirective;
import swim.api.server.ServerContext;
import swim.structure.Value;
import swim.warp.Envelope;

public class BasicPlane extends AbstractPlane {
  // Define policy; doesn't have to be an inner class
  class BasicPolicy extends AbstractPolicy {
    @Override
    protected <T> PolicyDirective<T> authorize(Envelope envelope, Identity identity) {
      if (identity != null) {
        final String token = identity.requestUri().getQuery().get("token");
        if ("abcd".equals(token)) {
          return allow();
        }
      }
      return forbid();
    }
  }

  @SwimRoute("/unit/:id")
  final AgentType<UnitAgent> unitAgentType = agentClass(UnitAgent.class);

  // Inject policy. Swim internally calls the no-argument constructor, which retains
  // its implicit call to super() in Java
  public BasicPlane() {
    context().setPlanePolicy(new BasicPolicy());
  }

  @Override
  public void didStart() {
    command("/unit/master", "WAKEUP", Value.absent());
  }

  @Override
  public void willStop() {
    System.out.println("Shutdown in progress...");
  }

  public static void main(String[] args) {
    final ServerContext server = ServerLoader.load(BasicPlane.class.getModule()).serverContext();
    server.start();
    final PlaneContext plane = server.getPlane("basic").planeContext();
    server.run();
    // observe the effects of our commands
    plane.downlinkValue()
      .nodeUri("/unit/master")
      .laneUri("info")
      .didSet((newValue, oldValue) -> {
        System.out.println("observed info change to " + newValue + " from " + oldValue);
      })
      .open();
    // Swim handles don't reject their own messages, regardless of policy
    plane.command("/unit/master", "publishInfo", Text.from("Without network"));
    // Network events without tokens get rejected
    plane.command("warp://localhost:9001", "/unit/master", "publishInfo", Text.from("With network, no token"));
    // Network events with the right token are accepted
    plane.command("warp://localhost:9001?token=abcd", "/unit/master", "publishInfo", Text.from("With network, token"));
  }
}

Try It Yourself

A standalone project that combines all of these snippets and handles any remaining boilerplate is available here.

arrow_back
previous

Map Lanes

next

Downlinks

arrow_forward