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.
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.
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; }
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; }
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
Now that the command is written and compiled, we need to somehow make it available for use with ROBOT.
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:
~/.robot/plugins
, in your own home directory;ROBOT_PLUGINS_DIRECTORY
environment variable;robot.pluginsdir
Java system property.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
.
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.