Repository navigation
Plugins
A plugin for the BARD backend API will present a REST resource as part of the core BARD resource hierarchy. This means that it will receive an HTTP request and should return an appropriate HTTP response. Being part of the BARD backend API means that plugin code will have access to internal API methods (such as those that access the database direcly). Since the BARD API is implemented in Java, plugins will be written in Java. More specifically plugins are deployed as Java servlets (but this does not preclude plugins in other languages. See below for further discussion).
- Each plugin represents an independent entity (or process) and can assume no dependencies, either in terms of libraries or other plugins. If external libraries are required they should be bundled with the plugin itself
- Plugins will employ Jersey annotations to indicate the resource path(s), method (GET, POST, PUT etc) and response type. Given that there are other libraries that support RESTful services, there is technically no restriction on their use. However the deployment mechanism prescribed by BARD assumes the use of Jersey
- Plugins should be RESTful as far as possible (particularly with respect to not maintaining state)
- A plugin need only know it's own root resource and does not need to know the preceding portions of the REST hierarchy. Since a plugin is a servlet, the context name must follow a prescribed format (described below)
- Currently plugins will not have write permissions to the backend database. Any local storage should be handled in the context of the Tomcat container (or via an external service)
- A plugin must provide a JSON manifest, accessible via
/_manifest. The response will include information on the provider, documentation, resource path, HTTP error codes, media types and so on. The JSON manifest is is defined by a schema.
A plugin must implement the IPlugin interface. This is available in bardplugin.jar, which is part of the plugin repository. There is no requirement that a plugin be located in a specific package, allowing the the implementer full flexibility in their package hierarchy. However, a plugin will require a certain set of annotations which are described below, and which will be validated at submission time.
The plugin class should provide an empty (i.e., no argument) constructor. The getDescription() method should be annotated with
@GET
@Produces("text/plain")
@Path("/_info")The getManifest() method should be annotated with
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/_manifest")Example plugins can be found in the plugin repository.
bardplugin.jar also provides the plugin access to the internal BARD API, notably the internal database classes, DBUtils. (Usage of DBUtils assumes that you have a local installation of the BARD data warehouse).
A common task for any plugin is to initialize resources. Since a plugin is meant to present a REST resource (via Jersey), this means that each time a request is made of a resource, the implementing class is instantiated. As a result, initialization code that is located in the class constructor will be called each time a request is made. In many scenarios this is the correct behavior, given that REST resources are meant to be stateless. In practice, however, this can be problematic. For example, initializing an in-memory database should only be done once, and not on every request.
To achieve one time initialization we make use of the fact that a plugin is basically a JavaEE Servlet. Thus a plugin should implement the ServletContextListener interface and place initialization code in the contextInitialized(ServletContextEvent sce) method. A class implementing this interface will be instantiated and the contextInitialized method will be called the first time the servlet is loaded.
If your plugin is a single class then that class could simply implement this interface. However, there is no restrictions and so a plugin could have a completely seperate class that only performs this initialization. (In such a case you'll want to make sure that variables initialized in this class can be access from other classes).
Having defined a class that implements ServletContextListener you will need to provide an entry in your web.xml to indicate to the container that this class should be called during servlet load time. You can simply paste
<listener>
<listener-class>class.to.use.at.load.time</listener-class>
</listener>
inside the web-app element (i.e., not inside the servlet element), replacing the value of listener-class with your fully qualified class name.
An example of a plugin that implements this form of one-time initialization is InitDemo and can be seen running here. This plugin highlights the difference between constructor initialization and servlet load time initialization by two static counters - one incremented in the contextInitialized method and one in the plugin class constructor. Refreshing the plugin page will increment the latter, but not the former.
The plugin (and it's dependencies) should be bundled as a [WAR](http://en.wikipedia.org/wiki/WAR_file_format_(Sun\)) file and submitted to NCGC for deployment in the main BARD backend. The structure of a plugin servlet is no different from standard servlets. Check out the examples and reuse as you see fit.
The only BARD specific requirements for the WAR file is that
- It must be named in the form
bardplugin_ROOTRESOURCE.war, whereROOTRESOURCEis the root of your resource hierarchy (i.e., the path used for the class level@Pathannotation) - The servlet mapping should be
/*. This requirement is currently a result of using Jersey. As long as your resources can be accessed using URL's of the formhttp://appcontainer/bardplugin_ROOTRESOURCE/ROOTRESOURCE/..., it doesn't matter how or what you specify for the servlet mapping
An important side effect of these constraints is that a given WAR file can only present a single top level REST resource. This may be relaxed in the future (so that you can bundle multiple REST resources into a single WAR file). Note that there is nothing preventing you from provide muliple (possibly completely unrelated) resources under your top level resource.
Thus the minimal web.xml that should be provided by the plugin WAR file is shown below
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<description>My Plugin Services</description>
<display-name>My Plugin Services</display-name>
<distributable/>
<servlet>
<description>My Plugin Services</description>
<display-name>My Plugin Services</display-name>
<servlet-name>MyPluginServletAdaptor</servlet-name>
<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
<!-- this must always be after init-param !-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>MyPluginServletAdaptor</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
Before submission the WAR file should be checked using the plugin validation tool.
Plugins are not forced to version their URLs. A version number should be specified in the manifest - this will be verified in the plugin validator. However, the validator will not check whether the resource paths provided by the plugin include a version This implies that a plugin provider may submit multiple versions of a given plugin. In such cases, the name of the class that provides the entry point to the plugin, must be different between different versions of the plugin.
It is important to realize that plugins that use data from the database to generate results, may not return the same results for a given version, when accessed at different times. This is because the data in the database will change over time.
As a side note, there are two views on API versioning - one approach says that versioning in the URL is a bad idea (and not REST-like) and instead a specific API version should be requested via the Accepts HTTP Header. The other approach says it's OK. See this thread for more details on the pros and cons of REST API versioning.
Since the plugin architecture is based on Tomcat deployments, plugins must be written in Java (or a language that compiles to the JVM and can be bundled as a WAR file. (Both Clojure and JRuby support this). However, the Java implementation can simply be a stub that accepts the incoming request and passes it on to an external URL, such as one hosted by the plugin provider. In this case, the service behind the external URL can be written in any language. The response from the external service would then be passed back to the caller. See the csls plugin that makes use of the Chemical Structure Lookup Service to perform a name to structure resolution.
The PluginValidator class implements the functionality to ensure that a submitted plugin follows the specification described here. Currently this is a single monolithic class that performs the checks for each constraint. In the future, we may redesign this class to be a more general validation framework, where checks for each constraint are modularized into distinct classes (similar in manner to the PMD architecture), allowing for easy enhancements or customized plugin validation schemes.
You can use the plugin validator class in your own code as follows. The tool can validate a plugin class or plugin WAR. An example of how you can validate a plugin class directly is given below:
import gov.nih.ncgc.bard.tools.PluginValidator;
...
PluginValidator v = new PluginValidator();
MyPlugin p = new MyPlugin();
boolean status = v.validate(p.getClass(), null);
System.out.println("status = " + status);
if (!status) {
for (String s : v.getErrors()) System.out.println(s);
}However, we recommend that you validate the plugin WAR, since it checks plugin classes as well as other constraints at the WAR level. Usage of the validator class in this case would be
import gov.nih.ncgc.bard.tools.PluginValidator;
...
PluginValidator v = new PluginValidator();
boolean status = v.validate("/path/to/bardplugin_myplugin.war");
System.out.println("status = " + status);
if (!status) {
for (String s : v.getErrors()) System.out.println(s);
}If running from your own code (or IDE), it is useful to manually set the CLASSPATH so that there are no inadvertant class conflicts. Usually this is not a problem if you are validating your own plugin, but might occur if you are validating someone elses plugin. The validator can also be run in stand alone mode from the command line. To build the stand alone validator run ant validator in the BARD directory to generate deploy/validator.jar. You can get a pre-built version of the validator from here.
Note that when running the validator you will get many lines output indicating IllegalAccessError, NoClassDefFoundError etc. These occurs since our dynamic class loading is not fully resolved. However, this does not affect the validation procedure. You can simply ignore output lines starting with INFO
Each plugin provides a method, getManifest() that returns a JSON string that describes the plugin. This manifest is defined by a JSON schema which defines the mandatory and optional elements. See the specification for more details on JSON schema itself. An example of a valid plugin manifest document is shown below
{
title:"BARD Assay Information",
description:"A brief description will go here",
version:"1.1",
author:"Rajarshi Guha",
authorEmail:"guhar@mail.nih.gov",
maintainer:"Rajarshi Guha",
maintainerEmail:"guhar@mail.nih.gov",
resources:[
{
path:"/{aid}/title",
mimetype:"text/plain",
method:"GET",
args:[
{
arg:"aid",
argtype:"integer"
}
]
},
{
path:"/{aid}/description",
mimetype:"text/plain",
method:"GET",
args:[
{
arg:"aid",
argtype:"integer"
}
]
}
]
}While it is relatively straightforward to generate the JSON manifest manually, BARD provides a helper class, PluginManifest. A plugin can instantiate this class and use the appropriate setters to provide the required information. The class can then generate the required JSON. An example of its usage is given below
PluginManifest pm = new PluginManifest();
pm.setAuthor("Rajarshi Guha");
pm.setAuthorEmail("guhar@mail.nih.gov");
pm.setMaintainer(pm.getAuthor());
pm.setMaintainerEmail(pm.getAuthorEmail());
pm.setTitle("BARD Assay Information");
pm.setDescription("A brief description will go here");
pm.setVersion(VERSION);
PluginManifest.PluginResource res1 = new PluginManifest.PluginResource();
res1.setPath("/{aid}/title");
res1.setMimetype("text/plain");
res1.setMethod("GET");
res1.setArgs(new PluginManifest.PathArg[]{new PluginManifest.PathArg("aid", "integer")});
PluginManifest.PluginResource res2 = new PluginManifest.PluginResource();
res2.setPath("/{aid}/description");
res2.setMimetype("text/plain");
res2.setMethod("GET");
res2.setArgs(new PluginManifest.PathArg[]{new PluginManifest.PathArg("aid", "integer")});
pm.setResources(new PluginManifest.PluginResource[]{res1, res2});
return pm.toJson();Clients that are interested in working with plugins can obtain a list of currently running plugins by visiting http://bard.nih.gov/api/latest/plugins/registry/list. The response is a JSON list where each element provides a summary of a plugin. The key element is the path field, which you can use to access the actual plugin. For example, based on the output from the registry, the http://www.farma.ku.dk/smartcyp/ plugin will be accessible at http://bard.nih.gov/api/latest/plugins/smartcyp. For most plugins, this URL will usually not return anything. However every BARD plugin will have two resources always present
-
/_info- that is meant to return a brief human readable summary of what the plugin does -
/_manifest- that returns a JSON string listing the available resources for that plugin and the data types that are expected as input and returned as output
Thus for the SMARTCyp plugin, visiting http://bard.nih.gov/api/latest/plugins/smartcyp/_manifest should return something like this
{
title:"SMARTCyp",
description:"SMARTCyp is a method for prediction of which sites in a molecule that are most liable to metabolism by Cytochrome P450. See http://www.farma.ku.dk/smartcyp/about.php for more details and the original code",
version:"1.1",
author:"Patrick Rydberg et al",
authorEmail:"pry@farma.ku.dk",
maintainer:"Rajarshi Guha",
maintainerEmail:"guhar@mail.nih.gov",
resources:[
{
path:"/summary",
mimetype:"text/html",
method:"GET",
args:[
{
arg:"smiles",
argtype:"string",
format:"query"
},
{
arg:"cid",
argtype:"number",
format:"query"
},
{
arg:"sid",
argtype:"number",
format:"query"
}
]
},
{
path:"/",
mimetype:"application/json",
method:"GET",
args:[
{
arg:"smiles",
argtype:"string",
format:"query"
},
{
arg:"cid",
argtype:"number",
format:"query"
},
{
arg:"sid",
argtype:"number",
format:"query"
}
]
}
]
}