Blog

Writing and using ROBOT plugins

ROBOT is a Java tool to manipulate OWL ontologies and especially Open Biological and Biomedical Ontologies. It provides a set of commands to perform diverse tasks on an ontology.

This document explains how to expand the set of available commands to perform more tasks than what is allowed by the standard distribution of ROBOT.

Familiarity with ROBOT and knowledge of Java development in general and development with the OWL API in particular is assumed.

1. Hello, World

We’ll illustrate how to expand the command set by creating a new hello command that inserts a “Hello, World”-type of message as an annotation into an existing ontology.

1.1. The basic code

A ROBOT command is a class that implements the org.obolibrary.robot.Command interface. Let’s create the skeleton of such a class:

package org.example.robot.hello;

import org.apache.commons.cli.Options;
import org.obolibrary.robot.Command;
import org.obolibrary.robot.CommandState;

public class HelloCommand implements Command {

    @Override
    public String getName() {
    }

    @Override
    public String getDescription() {
    }

    @Override
    public String getUsage() {
    }

    @Override
    public Options getOptions() {
    }

    @Override
    public void main(String[] args) {
    }

    @Override
    public CommandState execute(CommandState state, String[] args) throws Exception {
    }
}

The getName() method must return, well, the name of the command, as it should be invoked on the command line:

@Override public String getName() {
    return "hello";
}

The getDescription() method must return a one-line description of what the command is about, to be displayed in ROBOT’s main help menu. The getUsage() method must return a synopsis of the command line options expected by the command, to be displayed by ROBOT in the help message specific to that command:

@Override public String getDescription() {
    return "inject a hello annotation into the ontology";
}

@Override public String getUsage() {
    return "robot hello -r <RECIPIENT>";
}

The getOptions() method must return the list of options accepted by the command. For now, as hinted in the getUsage() method above, we accept one option, which indicates the “recipient” of the hello message (in Hello, World, the recipient is World). We must also accept the common options accepted by all other ROBOT commands. For that, we add a new options private variable which we initialise in the default constructor. We then just have to return that variable in getOptions():

import org.obofoundry.robot.CommandLineHelper;

public class HelloCommand implements Command {

    private Options options;

    public HelloCommand() {
        // Initialise options with ROBOT's common set of options
        options = CommandLineHelper.getCommonOptions();
        // Then add our own option.
        options.addOption("r", "recipient", true, "set the recipient of the hello message");
    }

    @Override
    public String getOptions() {
        return options;
    }

The main() method is pure boilerplate to satisfy the Command interface. It is merely a wrapper for the execute() method:

@Override
public void main(String[] args) {
    try {
        execute(null, args);
    } catch ( Exception e ) {
        CommandLineHelper.handleException(e);
    }
}

Finally, the execute() method is the core of the command. This is where the task should be performed. It takes as input an object representing the current state of ROBOT’s pipeline and the arguments passed to the command, and it must return the (possibly updated) state at the end.

// We need a few more imports
import org.apache.commons.cli.CommandLine;
import org.semanticweb.owlapi.model.AddOntologyAnnotation;
import org.semanticweb.owlapi.model.OWLAnnotation;
import org.semanticweb.owlapi.model.OWLDataFactory;
import org.semanticweb.owlapi.model.OWLOntology;
import org.semanticweb.owlapi.vocab.OWLRDFVocabulary;

public class HelloCommand implements Command {

    // [...]

    @Override
    public void execute(CommandState state, String[] args) {
        // Parse the command line
        CommandLine line = CommandLineHelper.getCommandLine(getUsage(), options, args);
        if ( line == null ) {
            return null;
        }

        // Get the recipient from the command line, or use default value
        String recipient = line.getOptionValue("recipient", "World");

        // Get the ontology that is being manipulated in the pipeline, and the associated OWLDataFactory
        OWLOntology ontology = state.getOntology();
        OWLDataFactory factory = ontology.getOWLOntologyManager().getOWLDataFactory();

        // Create and add an annotation to the ontology
        OWLAnnotation annot = factory.getOWLAnnotation(
            factory.getOWLAnnotationProperty(OWLRDFVocabulary.RDFS_COMMENT.getIRI()),
            factory.getOWLLiteral(String.format"Hello, %s", recipient))
        );
        ontology.getOWLOntologyManager().applyChange(new AddOntologyAnnotation(ontology, annot));

        return state;
    }

1.2. Adding I/O features

As currently implemented, the hello command can only be used in the middle of a ROBOT pipeline, after a command that takes care of providing an ontology to work with and before a command that will take care of saving it somewhere, as in the following example:

$ robot merge --input my_ontology.owl \
        hello --recipient Alice \
        convert --output my_annotated_ontology.owl

Proper ROBOT commands should be ready to work no matter at which point they are called in a pipeline. For that, the hello command must handle standard I/O-related options.

First, we import another ROBOT helper class:

import org.obolibrary.robot.IOHelper;

Then we need to declare the I/O options in the constructor:

public HelloCommand() {
    // Initialise options with ROBOT's common set of options
    options = CommandLineHelper.getCommonOptions();

    // Then add the standard I/O options
    options.addOption("i", "input", true, "load ontology from a file");
    options.addOption("I", "input-iri", true, "load ontology from an IRI");
    options.addOption("o", "output", true, "save ontology to a file");

    // Then add our own option.
    options.addOption("r", "recipient", true, "set the recipient of the hello message");
}

Warning: the I/O options must be named exactly as shown. This is required by the ROBOT I/O helper.

Finally, we can update the execute() method, in two places. At the beginning, after parsing the command line but before doing anything else:

@Override
public CommandState execute(CommandState state, String[] args) throws Exception {
    CommandLine line = CommandLineHelper.getCommandLine(getUsage(), options, args);
    if ( line == null ) {
        return null;
    }

    // Handle I/O options if needed
    IOHelper ioHelper = CommandLineHelper.getIOHelper(line);
    state = CommandLineHelper.updateInputOntology(ioHelper, state, line);

    // [...]

And at the end, after the task is done and before returning:

// [...]

    // Write the ontology in its current state if needed
    CommandLineHelper.maybeSaveOutput(line, state.getOntology());

    return state;
}

2. Building the command

Now that the Java implementation of the hello command is ready, it needs to be built. Here we’ll do that using Maven. Adapting the example to make it work with other build systems such as Gradle is left as an exercise to the reader.

Here is a minimal POM file:

<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.example</groupId>
  <artifactId>hello-command</artifactId>
  <version>0.1.0</version>

  <dependencies>
    <dependency>
      <groupId>org.obolibrary.robot</groupId>
      <artifactId>robot-command</artifactId>
      <version>1.9.5</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.0</version>
        <configuration>
          <release>8</release>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

The only dependency we need is robot-command, which provides (among other things) the Command interface and the CommandLineHelper class. All the other dependencies we need (such as the Apache Commons CLI library for option parsing or the OWL API for actually manipulating ontologies) are indirectly brought as dependencies of ROBOT.

Put the POM file at the top of the project directory and the HelloCommand.java file down in src/main/java/org/example/robot/hello, then build with:

$ mvn clean package

3. Making the new command available in ROBOT

Now that the command is written and compiled, we need to somehow make it available for use with ROBOT.

3.1. As a ROBOT “plugin”

Since version 1.9.5, ROBOT has native support for “pluggable commands”. A pluggable command is a Jar file containing at least one implementation of the org.obolibrary.robot.Command interface (plus any additional classes as needed), accompanied by a META-INF/services/org.obolibrary.robot.Command file that itself contains the fully qualified name of all the org.obolibrary.robot.Command implementations (one per line).

So in our example, to turn the hello command into a pluggable command, simply add a src/main/resources/META-INF/services/org.obolibrary.robot.Command file containing only this line:

org.example.robot.hello.HelloCommand

Then rebuild the package with mvn clean package.

You must then place the resulting Jar file into one of the directories where ROBOT will look for archives containing pluggable commands. Those directories are:

Importantly, the name of the Jar file (without the .jar extension) within the plugins directory will become part of the name of the pluggable command. Because of that:

In our example, we’ll put the file under the name myplugin.jar in ~/.robot/plugins. Let’s then call ROBOT without a command to confirm our hello command is available:

$ robot
usage: robot [command] [options] <arguments>
[... output truncated for brevity ...]
commands:
 help             print help for command
[... output truncated for brevity ...]
 verify           verify an ontology does not violate rules (as queries)
 myplugin:hello   inject a hello annotation into the ontology

Observe the presence of the myplugin:hello command. This command may now be called like any other ROBOT command:

$ robot myplugin:hello -i test.ofn -r Alice -o test-hello.ofn

That command would produce a test-hello.ofn ontology with the same contents as the input ontology test.ofn, but with an added rdfs:comment annotation with the value Hello, Alice.

3.2. In a customised distribution of ROBOT

Another method is to build a custom distribution of ROBOT where our command will be available as if it was a built-in command. This is the only available method if for any reason ROBOT 1.9.5 cannot be used.

For that, we first create a new org.example.robot.hello.StandaloneRobotCommand class (the name does not really matter) that will act as the entry point for our custom ROBOT:

package org.example.robot.hello;

import org.obolibrary.robot.*;

public class StandaloneRobotCommand {

    public static void main(String[] args) {
        CommandManager m = new CommandManager();

        // Add to the command manager all the standard commands
        m.addCommand("annotate", new AnnotateCommand());
        m.addCommand("collapse", new CollapseCommand());
        m.addCommand("convert", new ConvertCommand());
        m.addCommand("diff", new DiffCommand());
        m.addCommand("expand", new ExpandCommand());
        m.addCommand("explain", new ExplainCommand());
        m.addCommand("export", new ExportCommand());
        m.addCommand("export-prefixes", new ExportPrefixesCommand());
        m.addCommand("extract", new ExtractCommand());
        m.addCommand("filter", new FilterCommand());
        m.addCommand("materialize", new MaterializeCommand());
        m.addCommand("measure", new MeasureCommand());
        m.addCommand("merge", new MergeCommand());
        m.addCommand("mirror", new MirrorCommand());
        m.addCommand("python", new PythonCommand());
        m.addCommand("query", new QueryCommand());
        m.addCommand("reason", new ReasonCommand());
        m.addCommand("reduce", new ReduceCommand());
        m.addCommand("relax", new RelaxCommand());
        m.addCommand("remove", new RemoveCommand());
        m.addCommand("rename", new RenameCommand());
        m.addCommand("repair", new RepairCommand());
        m.addCommand("report", new ReportCommand());
        m.addCommand("template", new TemplateCommand());
        m.addCommand("unmerge", new UnmergeCommand());
        m.addCommand("validate-profile", new ValidateProfileCommand());
        m.addCommand("verify", new VerifyCommand());

        // Add our own command
        m.addCommand("hello", new HelloCommand());

        // Let the command manager do its job
        m.main(args);
    }
}

This is basically a simplified version of ROBOT’s original entry point, in which we simply add our own command in addition to the built-in ones.

Then we need to create a Jar file that will contain not only our own compiled Java classes, but the entire ROBOT distribution as well. We’ll use Maven’s shade plugin for that:

<build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</groupId>
        <version>3.2.4</version>
        <executions>
          <execution>
            <id>robot-standalone</id>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <finalName>my-custom-robot</finalName>
              <filters>
                <filter>
                  <artifact>*:*</artifact>
                  <excludes>
                    <exclude>META-INF/*.MF</exclude>
                    <exclude>META-INF/*.DSA</exclude>
                    <exclude>META-INF/*.SF</exclude>
                  </excludes>
                </filter>
              </filters>
              <transformers>
                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <mainClass>org.example.robot.hello.StandaloneRobotCommand</mainClass>
                </transformer>
              </transformers>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

Make sure the mainClass parameters points to our custom entry point above.

You can then build the package again:

$ mvn clean package

And start using the custom-built ROBOT:

$ java -jar target/my-custom-robot.jar
usage: robot [command] [options] <arguments>
[... output truncated for brevity ...]
commands:
 help             print help for command
[... output truncated for brevity ...]
 verify           verify an ontology does not violate rules (as queries)
 hello            inject a hello annotation into the ontology

You’ll have noticed that here, the hello command is not prefixed with myplugin:, as in the previous section. If it bothers you that the final command name differs depending on whether you’re using the command as a pluggable command or in custom-built ROBOT, you may manually add a prefix to the command name when you register the command: m.addCommand("myplugin:hello", new HelloCommand());.

Even if you do have a version of ROBOT that already supports pluggable commands, building a customised ROBOT as above is helpful to develop and debug pluggable commands, as you end up with a Jar file that contains everything you need to use (and so, test) your commands, without having to put them into a ROBOT plugins directory.

Note that if you distribute your custom-built ROBOT instead of only using it for yourself, you must do so in compliance with ROBOT’s BSD-style license, since you’re not only distributing your own code but ROBOT’s code as well.