Skip to content

Commit

Permalink
Merge pull request #1142 from gouttegd/wip/simple-plugins-test
Browse files Browse the repository at this point in the history
Allow to load extra commands from plugins (with test)
  • Loading branch information
jamesaoverton authored Sep 20, 2023
2 parents 0e0c7da + 04585f2 commit 87146ea
Show file tree
Hide file tree
Showing 10 changed files with 358 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Updated ELK from 0.4.3 to 0.5.0. [#999]. This is an important change as ELK 0.5.0 is more complete than 0.4.3, which means that it will potentially uncover inferences, in particular unsatisfiable classes, which were not recognised by ELK 0.4.3.
- Add support for pluggable commands [#1119]

## [1.9.4] - 2023-05-23

Expand Down Expand Up @@ -367,6 +369,7 @@ First official release of ROBOT!
[`template`]: http://robot.obolibrary.org/template
[`validate`]: http://robot.obolibrary.org/validate

[#1119]: https://github.com/ontodev/robot/pull/1119
[#1100]: https://github.com/ontodev/robot/pull/1100
[#1091]: https://github.com/ontodev/robot/issues/1091
[#1089]: https://github.com/ontodev/robot/issues/1089
Expand All @@ -382,6 +385,7 @@ First official release of ROBOT!
[#1016]: https://github.com/ontodev/robot/issues/1016
[#1009]: https://github.com/ontodev/robot/issues/1009
[#1000]: https://github.com/ontodev/robot/pull/1000
[#999]: https://github.com/ontodev/robot/pull/999
[#979]: https://github.com/ontodev/robot/pull/979
[#978]: https://github.com/ontodev/robot/pull/978
[#977]: https://github.com/ontodev/robot/pull/977
Expand Down
1 change: 1 addition & 0 deletions docs/_layouts/default.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<a href="/chaining">chaining commands</a><br>
<a href="/global">global options</a><br>
<a href="/make">makefile</a><br>
<a href="/plugins">plugins</a><br>
- - - - - - - - - -<br>
<a href="/annotate">annotate</a><br>
<a href="/collapse">collapse</a><br>
Expand Down
36 changes: 36 additions & 0 deletions docs/plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Plugins

The set of ROBOT commands can be extended locally with plugins. A ROBOT plugin is a Java archive file (`.jar`) providing one or more supplementary commands (hereafter called "pluggable commands").

## Using plugins

ROBOT searches for plugins in the following locations:

* the `.robot/plugins` directory in the current user's home directory;
* the directory specified by the Java system property `robot.pluginsdir`, if such a property is set;
* the directory specified by the environment variable `ROBOT_PLUGINS_DIRECTORY`, if such a variable is set in the environment.

Installing a plugin is therefore simply a matter of either

* placing the Jar file into your `~/.robot/plugins` directory, or
* placing the Jar file into any directory and making sure ROBOT knows to search that directory, by setting the `robot.pluginsdir` system property or the `ROBOT_PLUGINS_DIRECTORY` environment variable accordingly.

Importantly, the basename of the Jar file (without the `.jar` extension) within the directory will become part of the name of any pluggable command provided by the plugin. For example, if the file is named `myplugin.jar` and it provides a command called `mycommand`, that command will be available under the name `myplugin:mycommand`. Because of that:

* the name of the Jar file **must** be in lowercase only;
* the name **should** be kept short and simple.

Once the plugin is installed, any pluggable command it provides is immediately available to ROBOT. You can check by calling `robot` without any argument to get it to print the full list of available commands, which will include the commands provided by installed plugins, if any.

## Creating plugins

A pluggable command, just like any other ROBOT command, is a Java class that implements the `org.obolibrary.robot.Command` interface. A plugin is Java archive file that contains at least:

* the compiled Java code ("bytecode") for at least one class implementing the `org.obolibrary.robot.Command` interface, and
* a `META-INF/services/org.obolibrary.robot.Command` file that list all implementations of that interface available in the archive (one per line).

For example, if the command `mycommand` is implemented in a class named `MyCommand` in the package `org.example.myplugin`, the `META-INF/services/org.obolibrary.robot.Command` file must contain a single line `org.example.myplugin.MyCommand`.

In addition to the class implementing the command itself, the archive must also provide any additional classes that may be required for the command to work. This must include classes from any external dependency, unless that dependency also happens to be a dependency of ROBOT itself (for example, there is no need for the archive to contain a copy of the classes of the OWL API, since they are already present in the standard distribution of ROBOT).

A more detailed walkthrough of how to create a plugin is available [here](https://incenp.org/notes/2023/writing-robot-plugins.html).
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
<module>robot-core</module>
<module>robot-command</module>
<module>robot-maven-plugin</module>
<module>robot-mock-plugin</module>
</modules>

<profiles>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ private static CommandManager initManager() {
m.addCommand("unmerge", new UnmergeCommand());
m.addCommand("validate-profile", new ValidateProfileCommand());
m.addCommand("verify", new VerifyCommand());

PluginManager pm = new PluginManager();
pm.addPluggableCommands(m);

return m;
}

Expand Down
106 changes: 106 additions & 0 deletions robot-command/src/main/java/org/obolibrary/robot/PluginManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package org.obolibrary.robot;

import java.io.File;
import java.io.FilenameFilter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Pluggable commands loader.
*
* @author <a href="mailto:[email protected]">Damien Goutte-Gattat</a>
*/
public class PluginManager {

private static final Logger logger = LoggerFactory.getLogger(PluginManager.class);

private HashMap<String, URL> jars = null;

/**
* Find pluggable commands and add them to a CommandManager.
*
* @param cm the command manager to add commands to
*/
public void addPluggableCommands(CommandManager cm) {
if (jars == null) {
findPlugins();
}

loadPlugin(cm, null, "");
for (String pluginBasename : jars.keySet()) {
loadPlugin(cm, jars.get(pluginBasename), pluginBasename + ":");
}
}

/**
* Load pluggable commands from a Jar file.
*
* @param cm the command manager to add commands to
* @param jarFile the Jar file to load commands from; if null, will attempt to find pluggable
* commands in the system class path
* @param prefix a string to prepend to the name of each pluggable command when adding them to the
* command manager
*/
private void loadPlugin(CommandManager cm, URL jarFile, String prefix) {
ClassLoader classLoader =
jarFile != null
? URLClassLoader.newInstance(new URL[] {jarFile})
: URLClassLoader.getSystemClassLoader();

try {
ServiceLoader<Command> serviceLoader = ServiceLoader.load(Command.class, classLoader);
for (Command pluggableCommand : serviceLoader) {
cm.addCommand(prefix + pluggableCommand.getName(), pluggableCommand);
}
} catch (ServiceConfigurationError e) {
logger.warn("Invalid configuration in plugin %s, ignoring plugin", jarFile);
}
}

/**
* Detect Jar files in a set of directories. If a Jar file with the same basename is found in more
* than one directory, the last one found takes precedence.
*/
private void findPlugins() {
String[] pluginsDirectories = {
System.getProperty("robot.pluginsdir"),
System.getenv("ROBOT_PLUGINS_DIRECTORY"),
new File(System.getProperty("user.home"), ".robot/plugins").getPath()
};
FilenameFilter jarFilter =
new FilenameFilter() {
@Override
public boolean accept(File file, String name) {
return name.endsWith(".jar");
}
};

jars = new HashMap<String, URL>();

for (String directoryName : pluginsDirectories) {
if (directoryName == null || directoryName.length() == 0) {
continue;
}

File directory = new File(directoryName);
if (directory.isDirectory()) {
for (File jarFile : directory.listFiles(jarFilter)) {
try {
String basename = jarFile.getName();
basename = basename.substring(0, basename.length() - 4);
jars.put(basename, jarFile.toURI().toURL());
} catch (MalformedURLException e) {
// This should never happen: the URL is constructed by the Java Class Library
// from a real filename, it should never be malformed.
}
}
}
}
}
}
53 changes: 53 additions & 0 deletions robot-mock-plugin/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.obolibrary.robot</groupId>
<artifactId>robot</artifactId>
<version>1.10.0-SNAPSHOT</version>
</parent>
<artifactId>robot-mock-plugin</artifactId>
<name>robot-mock-plugin</name>
<description>A dummy ROBOT plugin for testing and demonstration purposes.</description>

<build>
<plugins>
<!-- Enforce Google Java Style -->
<plugin>
<groupId>com.coveo</groupId>
<artifactId>fmt-maven-plugin</artifactId>
<version>2.9</version>
<executions>
<execution>
<goals>
<goal>format</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.0.0-M5</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

<dependencies>
<dependency>
<groupId>org.obolibrary.robot</groupId>
<artifactId>robot-command</artifactId>
<version>${project.parent.version}</version>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package org.obolibrary.robot.plugins;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Options;
import org.obolibrary.robot.Command;
import org.obolibrary.robot.CommandLineHelper;
import org.obolibrary.robot.CommandState;
import org.obolibrary.robot.IOHelper;
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;

/** A dummy pluggable command for testing and demonstration purposes. */
public class HelloCommand implements Command {

private Options options;

public HelloCommand() {
options = CommandLineHelper.getCommonOptions();
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");
options.addOption("r", "recipient", true, "set the recipient of the hello message");
}

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

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

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

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

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

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

IOHelper ioHelper = CommandLineHelper.getIOHelper(line);
state = CommandLineHelper.updateInputOntology(ioHelper, state, line);

String recipient = line.getOptionValue("recipient", "World");

OWLOntology ontology = state.getOntology();
OWLDataFactory factory = ontology.getOWLOntologyManager().getOWLDataFactory();

OWLAnnotation annot =
factory.getOWLAnnotation(
factory.getOWLAnnotationProperty(OWLRDFVocabulary.RDFS_COMMENT.getIRI()),
factory.getOWLLiteral(String.format("Hello, %s", recipient)));
ontology.getOWLOntologyManager().applyChange(new AddOntologyAnnotation(ontology, annot));

CommandLineHelper.maybeSaveOutput(line, state.getOntology());

return state;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.obolibrary.robot.plugins.HelloCommand
Loading

0 comments on commit 87146ea

Please sign in to comment.