26.2. The Plug-in API

26.2.1. The main plug-in interfaces
The net.sf.basedb.core.plugin.Plugin interface
The net.sf.basedb.core.plugin.InteractivePlugin interface
26.2.2. How the BASE core interacts with the plug-in when...
Installing a plug-in
Configuring a plug-in
Checking if a plug-in can be used in a given context
Creating a new job
Executing a job
26.2.3. Abort a running a plug-in
26.2.4. Using custom JSP pages for parameter input

26.2.1. The main plug-in interfaces

The BASE API defines two interfaces and one abstract class that are vital for implementing plug-ins:

  • net.sf.basedb.core.plugin.Plugin
  • net.sf.basedb.core.plugin.InteractivePlugin
  • net.sf.basedb.core.plugin.AbstractPlugin

A plug-in must always implement the Plugin interface. The InteractivePlugin interface is optional, and is only needed if you want user interaction. The AbstractPlugin is a useful base class that your plug-in can use as a superclass. It provides default implementations for some of the interface methods and also has utility methods for validating and storing job and configuration parameter values. Another reason to use this class as a superclass is that it will shield your plug-in from future changes to the Plug-in API. For example, if we decide that a new method is needed in the Plugin interface we will also try to add a default implementation in the AbstractPlugin class.

[Important] Important

A plug-in must also have public no-argument contructor. Otherwise, BASE will not be able to create new instances of the plug-in class.

The net.sf.basedb.core.plugin.Plugin interface

This interface defines the following methods and must be implemented by all plug-ins.

public Plugin.MainType getMainType();

Return information about the main type of plug-in. The Plugin.MainType is an enumeration with five possible values:

  • ANALYZE: An analysis plug-in

  • EXPORT: A plug-in that exports data

  • IMPORT: A plug-in that imports data

  • INTENSITY: A plug-in that calculates the original spot intensities from raw data

  • OTHER: Any other type of plug-in

The returned value is stored in the database but is otherwise not used by the core. Client applications (such as the web client) will probably use this information to group the plug-ins, i.e., a button labeled Export will let you select among the export plug-ins.

Example 26.2. A typical implementation just return one of the values

public Plugin.MainType getMainType()
{
   return Plugin.MainType.OTHER;
}

public boolean supportsConfigurations();

If this method returns true, the plug-in can have different configurations, (i.e. PluginConfiguration). Note that this method may return true even if the InteractivePlugin interface is not implemented. The AbstractPlugin returns true for this method, which is the old way before the introduction of this method.

public boolean requiresConfiguration();

If this method returns true, the plug-in must have a configuration to be able to run. For example, some of the core import plug-ins must have information about the file format, to be able to import any data. The AbstractPlugin returns false for this method, which is the old way before the introduction of this method.

public Collection<Permissions> getPermissions();

Return a collection of permissions that the plug-in needs to be able to function as expected. This method may return null or an empty collection. In this case the plug-in permission system is not used and the plug-in always gets the same permissions as the logged in user. If permissions are specified the plug-in should list all permissions it requires. Permissions that are not listed are denied.

[Note] Note

The final assignment of permissions to a plug-in is always at the hands of a server administrator. He/she may decide to disable the plug-in permission system or revoke some of the requested permissions. The permissions returned by this method is only a recommendation that the server administrator may or may not accept. See Section 22.1.6, “Plug-in permissions” for more information about plug-in permissions.

public void init(SessionControl sc,
                 ParameterValues configuration,
                 ParameterValues job)
    throws BaseException;

Prepare the plug-in for execution or configuration. If the plug-in needs to do some initialization this is the place to do it. A typical implementation however only stores the passed parameters in instance variables for later use. Since it is not possible to know what the user is going to do at this stage, we recommend lazy initialisation of all other resources.

The parameters passed to this method has vital information that is needed to execute the plug-in. The SessionControl is a central core object holding information about the logged in user and is used to create DbControl objects which allows a plug-in to connect to the database to read, add or update information. The two ParameterValues objects contain information about the configuration and job parameters to the plug-in. The configuration object holds all parameters stored together with a PluginConfiguration object in the database. If the plug-in is started without a configuration this object is null. The job object holds all parameters that are stored together with a Job object in the database. This object is null if the plug-in is started without a job.

The difference between a configuration parameter and a job parameter is that a configuration is usually something an administrator sets up, while a job is an actual execution of a plug-in. For example, a configuration for an import plug-in holds the regular expressions needed to parse a text file and find the headers, sections and data lines, while the job holds the file to parse.

The AbstractPlugin contains an implementation of this method that saves the passed objects in protected instance variables. If you override this method we recommend that you also call super.init().

Example 26.3. The AbstractPlugin implementation of Plugin.init()

protected SessionControl sc = null;
protected ParameterValues configuration = null;
protected ParameterValues job = null;
/**
   Store copies of the session control, plug-in and job configuration. These
   are available to subclasses in the {@link #sc}, {@link #configuration}
   and {@link #job} variables. If a subclass overrides this method it is 
   recommended that it also calls super.init(sc, configuration, job).
*/
public void init(SessionControl sc, 
   ParameterValues configuration, ParameterValues job)
   throws BaseException
{
   this.sc = sc;
   this.configuration = configuration;
   this.job = job;
}

public void run(Request request,
                Response response,
                ProgressReporter progress);

Run the plug-in.

The request parameter is of historical interest only. It has no useful information and can be ignored.

The progress parameter should be used by a plug-in to report its progress back to the core. The core will usually send the progress information to the database, which allows users to see exactly how the plug-in is progressing from the web interface. This parameter is allowed to be null, but BASE will always use a progress reporter. The plug-in should try to not over-use the progress reporter. The default implementation used by BASE has a time threshold so that calls that occur too often with too little change in the progress are ignored. A good starting point is to divide the work into 100 pieces each representing 1% of the work, i.e., if the plug-in should export 100 000 items it should report progress after every 1000 items.

The response parameter is used to tell the core if the plug-in was successful or failed. Not setting a response is considered a failure by the core. From the run method it is only allowed to use on of the Response.setDone(), Response.setError() or Response.setContinue() methods.

[Important] Important
It is also considered bad practice to let exceptions escape out from this method. Always use try...catch to catch exceptions and use Response.setError() to report the error back to the core.

Example 26.4.  Here is a skeleton for the run() method

public void run(Request request, Response response, ProgressReporter progress)
{
   // Open a connection to the database
   // sc is set by init() method
   DbControl dc = sc.newDbControl();
   try
   {
      // Insert code for plug-in here

      // Commit the work
      dc.commit();
      response.setDone("Plug-in ended successfully");
   }
   catch (Throwable t)
   {
      // All exceptions must be catched and sent back 
      // using the response object
      response.setError(t.getMessage(), Arrays.asList(t));
   }
   finally
   {
      // IMPORTANT!!! Make sure opened connections are closed
      if (dc != null) dc.close();
   }
}

public void done();

Clean up all resources after executing the plug-in. This method must not throw any exceptions.

Example 26.5.  The AbstractPlugin contains an implementation of the done() method simply sets the parameters passed to the init() method to null

/**
   Clears the variables set by the init method. If a subclass 
   overrides this method it is recommended that it also calls super.done().
*/
public void done()
{
   configuration = null;
   job = null;
   sc = null;
}

The net.sf.basedb.core.plugin.InteractivePlugin interface

If you want the plug-in to be able to interact with the user you must also implement this interface. This is probably the case for most plug-ins. Among the core plug-ins shipped with BASE the SpotImageCreator is one plug-in that does not interact with the user. Instead, the web client has special JSP pages that handles all the interaction, creates a job for it and sets the parameters. This approach can also be used for other plug-ins if, for example, an extension is used to provide the gui.

The InteractivePlugin has three main tasks:

  1. Tell a client application where the plug-in should be plugged in.

  2. Ask the users for configuration and job parameters.

  3. Validate parameter values entered by the user and store those in the database.

This requires that the following methods are implemented.

public Set<GuiContext> getGuiContexts();

Return information about where the plug-in should be plugged in. Each place is identified by a GuiContext object, which is an Item and a Type. The item is one of the objects defined by the Item enumeration and the type is either Type.LIST or Type.ITEM, which corresponde to the list view and the single-item view in the web client.

For example, the GuiContext = (Item.REPORTER, Type.LIST) tells a client application that this plug-in can be plugged in whenever a list of reporters is displayed. The GuiContext = (Item.REPORTER, Type.ITEM) tells a client application that this plug-in can be plugged in whenever a single reporter is displayed. The first case may be appropriate for a plug-in that imports or exports reporters. The second case may be used by a plug-in that updates the reporter information from an external source (well, it may make sense to use this in the list case as well).

The returned information is copied by the core at installation time to make it easy to ask for all plug-ins for a certain GuiContext.

A typical implementation creates a static unmodifiable Set which is returned by this method. It is important that the returned set cannot be modified. It may be a security issue if a misbehaving client application does that.

Example 26.6.  A typical implementation of getGuiContexts

// From the net.sf.basedb.plugins.RawDataFlatFileImporter plug-in
private static final Set<GuiContext> guiContexts = 
   Collections.singleton(new GuiContext(Item.RAWBIOASSAY, GuiContext.Type.ITEM));

public Set<GuiContext> getGuiContexts()
{
   return guiContexts;
}

public String isInContext(GuiContext context,
                          Object item);

This method is called to check if a particular item is usable for the plug-in. This method is invoked to check if a plug-in can be used in a given context. If invoked from a list context the item parameter is null. The plug-in should return null if it finds that it can be used. If the plug-in can't be used it must decide if the reason should be a warning or an error condition.

A warning is issued by returning a string with the warning message. It should be used when the plug-in can't be used because it is unrelated to the current task. For example, a plug-in for importing Genepix data should return a warning when somebody wants to import data to an Agilent raw bioassay.

An error message is issued by throwing an exception. This should be used when the plug-in is related to the current task but still can't do what it is supposed to do. For example, trying to import raw data if the logged in user doesn't have write permission to the raw bioassay.

As a rule of thumb, if there is a chance that another plug-in might be able to perform the same task a warning should be used. If it is guaranteed that no other plug-in can do it an error message should be used.

Here is a real example from the RawDataFlatFileImporter plug-in which imports raw data to a RawBioAssay. Thus, GuiContext = (Item.RAWBIOASSAY, Type.ITEM), but the plug-in can only import data if the logged in user has write permission, there is no data already, and if the raw bioassay has the same raw data type as the plug-in has been configured for.

Example 26.7.  A realistic implementation of the isInContext() method

/**
   Returns null if the item is a {@link RawBioAssay} of the correct
   {@link RawDataType} and doesn't already have spots.
   @throws PermissionDeniedException If the raw bioasssay already has raw data
   or if the logged in user doesn't have write permission
*/
public String isInContext(GuiContext context, Object item)
{
   String message = null;
   if (item == null)
   {
      message = "The object is null";
   }
   else if (!(item instanceof RawBioAssay))
   {
      message = "The object is not a RawBioAssay: " + item;
   }
   else
   {
      RawBioAssay rba = (RawBioAssay)item;
      String rawDataType = (String)configuration.getValue("rawDataType");
      RawDataType rdt = rba.getRawDataType();
      if (!rdt.getId().equals(rawDataType))
      {
         // Warning
         message = "Unsupported raw data type: " + rba.getRawDataType().getName();
      }
      else if (!rdt.isStoredInDb())
      {
         // Warning
         message = "Raw data for raw data type '" + rdt + "' is not stored in the database";
      }
      else if (rba.hasData())
      {
         // Error
         throw new PermissionDeniedException("The raw bioassay already has data.");
      }
      else
      {
         // Error
         rba.checkPermission(Permission.WRITE);
      }
   }
   return message;		
}

public RequestInformation getRequestInformation(GuiContext context,
                                                String command)
    throws BaseException;

Ask the plug-in for parameters that need to be entered by the user. The GuiContext parameter is one of the contexts returned by the getGuiContexts method. The command is a string telling the plug-in what command was executed. There are two predefined commands but as you will see the plug-in may define its own commands. The two predefined commands are defined in the net.sf.basedb.core.plugin.Request class.

Request.COMMAND_CONFIGURE_PLUGIN

Used when an administrator is initiating a configuration of the plug-in.

Request.COMMAND_CONFIGURE_JOB

Used when a user has selected the plug-in for running a job.

Given this information the plug-in must return a RequestInformation object. This is simply a title, a description, and a list of parameters. Usually the title will end up as the input form title and the description as a help text for the entire form. Do not put information about the individual parameters in this description, since each parameter has a description of its own.

Example 26.8.  When running an import plug-in it needs to ask for the file to import from and if existing items should be updated or not

// The complete request information
private RequestInformation configure Job;

// The parameter that asks for a file to import from
private PluginParameter<File> file Parameter;

// The parameter that asks if existing items should be updated or not
private PluginParameter<Boolean> updateExistingParameter;

public RequestInformation getRequestInformation(GuiContext context, String command)
   throws BaseException
{
   RequestInformation requestInformation = null;
   if (command.equals(Request.COMMAND_CONFIGURE_PLUGIN))
   {
      requestInformation = getConfigurePlugin();
   }
   else if (command.equals(Request.COMMAND_CONFIGURE_JOB))
   {
      requestInformation = getConfigureJob();
   }
   return requestInformation;
}

/**
   Get (and build) the request information for starting a job.
*/
private RequestInformation getConfigureJob()
{
   if (configureJob == null)
   {
      // A file is required
      fileParameter = new PluginParameter<File>(
         "file",
         "File",
         "The file to import the data from",
         new FileParameterType(null, true, 1)
      ); 
		  
      // The default value is 'false'
      updateExistingParameter = new PluginParameter<Boolean>(
         "updateExisting",
         "Update existing items",
         "If this option is selected, already existing items will be updated " +
         " with the information in the file. If this option is not selected " +
         " existing items are left untouched.",
         new BooleanParameterType(false, true)
      );

      List<PluginParameter<?>> parameters = 
         new ArrayList<PluginParameter<?>>(2);
      parameters.add(fileParameter);
      parameters.add(updateExistingParameter);
			
      configureJob = new RequestInformation
      (
         Request.COMMAND_CONFIGURE_JOB,
         "Select a file to import items from",
         "Description",
         parameters
      );
   }
   return configureJob;
}

As you can see it takes a lot of code to put together a RequestInformation object. For each parameter you need one PluginParameter object and one ParameterType object. To make life a little easier, a ParameterType can be reused for more than one PluginParameter.

StringParameterType stringPT = new StringParameterType(255, null, true);
PluginParameter one = new PluginParameter("one", "One", "First string", stringPT);
PluginParameter two = new PluginParameter("two", "Two", "Second string", stringPT);
// ... and so on

The ParameterType is an abstract base class for several subclasses each implementing a specific type of parameter. The list of subclasses may grow in the future, but here are the most important ones currently implemented.

[Note] Note

Most parameter types include support for supplying a predefined list of options to select from. In that case the list will be displayed as a drop-down list for the user, otherwise a free input field is used.

StringParameterType

Asks for a string value. Includes an option for specifying the maximum length of the string.

FloatParameterType, DoubleParameterType, IntegerParameterType, LongParameterType

Asks for numerical values. Includes options for specifying a range (min/max) of allowed values.

BooleanParameterType

Asks for a boolean value.

DateParameterType

Asks for a date.

FileParameterType

Asks for a file item.

ItemParameterType

Asks for any other item. This parameter type requires that a list of options is supplied, except when the item type asked for matches the current GuiContext, in which case the currently selected item is used as the parameter value.

PathParameterType

Ask for a path to a file or directory. The path may be non-existing and should be used when a plug-in needs an output destination, i.e., the file to export to, or a directory where the output files should be placed.

You can also create a PluginParameter with a null name and ParameterType. In this case, the web client will not ask for input from the user, instead it is used as a section header, allowing you to group parameters into different sections which increase the readability of the input parameters page.

PluginParameter firstSection = new PluginParameter(null, "First section", null, null);
PluginParameter secondSection = new PluginParameter(null, "Second section", null, null);
// ...

parameters.add(firstSection);
parameters.add(firstParameterInFirstSection);
parameters.add(secondParameteInFirstSection);

parameters.add(secondSection);
parameters.add(firstParameterInSecondSection);
parameters.add(secondParameteInSecondSection);
public void configure(GuiContext context,
                      Request request,
                      Response response);

Sends parameter values entered by the user for processing by the plug-in. The plug-in must validate that the parameter values are correct and then store them in database.

[Important] Important

No validation is done by the core, except converting the input to the correct object type, i.e. if the plug-in asked for a Float the input string is parsed and converted to a Float. If you have extended the AbstractPlugin class it is very easy to validate the parameters with the AbstractPlugin.validateRequestParameters() method. This method takes the same list of PluginParameter:s as used in the RequestInformation object and uses that information for validation. It returns null or a list of Throwable:s that can be given directly to the response.setError() methods.

When the parameters have been validated, they need to be stored in the database. Once again, it is very easy, if you use one of the AbstractPlugin.storeValue() or AbstractPlugin.storeValues() methods.

The configure method works much like the Plugin.run() method. It must return the result in the Response object, and should not throw any exceptions.

Example 26.9.  Configuration implementation building on the examples above

public void configure(GuiContext context, Request request, Response response)
{
   String command = request.getCommand();
   try
   {
      if (command.equals(Request.COMMAND_CONFIGURE_PLUGIN))
      {
         // TODO
      }
      else if (command.equals(Request.COMMAND_CONFIGURE_JOB))
      {
         // Validate user input
         List<Throwable> errors = 
            validateRequestParameters(getConfigureJob().getParameters(), request);
         if (errors != null)
         {
            response.setError(errors.size() +
               " invalid parameter(s) were found in the request", errors);
            return;
         }
         
         // Store user input
         storeValue(job, request, fileParameter);
         storeValue(job, request, updateExistingParameter);
         
         // We are happy and done
         File file = (File)job.getValue("file");
         response.setSuggestedJobName("Import data from file " + file.getName());
         response.setDone("Job configuration complete", Job.ExecutionTime.SHORT);
      }
   }
   catch (Throwable ex)
   {
      response.setError(ex.getMessage(), Arrays.asList(ex));
   }
}

Note that the call to response.setDone() has a second parameter Job.ExecutionTime.SHORT. It is an indication about how long time it will take to execute the plug-in. This is of interest for job queue managers which probably does not want to start too many long-running jobs at the same time blocking the entire system. Please try to use this parameter wisely and not use Job.ExecutionTime.SHORT out of old habit all the time.

The Response class also has a setContinue() method which tells the core that the plug-in needs more parameters, i.e. the core will then call getRequestInformation() again with the new command, let the user enter values, and then call configure() with the new values. This process is repeated until the plug-in reports that it is done or an error occurs.

[Tip] Tip

You do not have to store all values the plug-in asked for in the first place. You may even choose to store different values than those that were entered. For example, you might ask for the mass and height of a person and then only store the body mass index, which is calculated from those values.

An important note is that during this iteration it is the same instance of the plug-in that is used. However, no parameter values are stored in the database until the plugin sends a response.setDone(). After that, the plug-in instance is usually discarded, and a job is placed in the job queue. The execution of the plug-in happens in a new instance and maybe on a different server. This means that a plug-in can't store state from the configuration phase internally and expect it to be there in the execution phase. Everything the plug-in needs to do it's job must be stored as parameters in the database.

The only exception to the above rule is if the plug-in answers with Response.setExecuteImmediately() or Response.setDownloadImmediately(). Doing so bypasses the entire job queue system and requests that the job is started immediately. This is a permission that has to be granted to each plug-in by the server administrator. If the plug-in has this permission, the same object instance that was used in the configuration phase is also used in the execution phase. This is the only case where a plug-in can retain internal state between the two phases.

26.2.2. How the BASE core interacts with the plug-in when...

This section describes how the BASE core interacts with the plug-in in a number of use cases. We will outline the order the methods are invoked on the plug-in.

Installing a plug-in

When a plug-in is installed the core is eager to find out information about the plug-in. To do this it calls the following methods in this order:

  1. A new instance of the plug-in class is created. The plug-in must have a public no-argument constructor.

  2. Calls are made to Plugin.getMainType(), Plugin.supportsConfigurations() and Plugin.requiresConfiguration() to find out information about the plug-in. This is the only time these methods are called. The information that is returned by them are copied and stored in the database for easy access.

    [Note] Note

    The Plugin.init() method is never called during plug-in installation.

  3. If the plug-in implements the InteractivePlugin interface the InteractivePlugin.getGuiContexts() method is called. This is the only time this method is called and the information it returns are copied and stored in the database.

  4. If the server admin decided to use the plug-in permission system, the Plugin.getPermissions() method is called. The returned information is copied and stored in the database.

Configuring a plug-in

The plug-in must implement the InteractivePlugin interface and the Plugin.supportsConfigurations() method must return TRUE. The configuration is done with a wizard-like interface (see Section 22.2.1, “Configuring plug-in configurations”). The same plug-in instance is used throughout the entire configuration sequence.

  1. A new instance of the plug-in class is created. The plug-in must have a public no-argument constructor.

  2. The Plugin.init() method is called. The job parameter is null.

  3. The InteractivePlugin.getRequestInformation() method is called. The context parameter is null and the command is the value of the string constant Request.COMMAND_CONFIGURE_PLUGIN (_config_plugin).

  4. The web client process the returned information and displays a form for user input. The plug-in will have to wait some time while the user enters data.

  5. The InteractivePlugin.configure() method is called. The context parameter is still null and the request parameter contains the parameter values entered by the user.

  6. The plug-in must validate the values and decide whether they should be stored in the database or not. We recommend that you use the methods in the AbstractPlugin class for this.

  7. The plug-in can choose between three different respones:

    • Response.setDone(): The configuration is complete. The core will write any configuation changes to the database, call the Plugin.done() method and then discard the plug-in instance.

    • Response.setError(): There was one or more errors. The web client will display the error messages for the user and allow the user to enter new values. The process continues with step 4.

    • Response.setContinue(): The parameters are correct but the plug-in wants more parameters. The process continues with step 3 but the command has the value that was passed to the setContinue() method.

Checking if a plug-in can be used in a given context

If the plug-in is an InteractivePlugin it has specified in which contexts it can be used by the information returned from InteractivePlugin.getGuiContexts() method. The web client uses this information to decide whether, for example, a Run plugin button should be displayed on a page or not. However, this is not always enough to know whether the plug-in can be used or not. For example, a raw data importer plug-in cannot be used to import raw data if the raw bioassay already has data. So, when the user clicks the button, the web client will load all plug-ins that possibly can be used in the given context and let each one of them check whether they can be used or not.

  1. A new instance of the plug-in class is created. The plug-in must have a public no-argument constructor.

  2. The Plugin.init() method is called. The job parameter is null. The configuration parameter is null if the plug-in does not have any configuration parameters.

  3. The InteractivePlugin.isInContext() is called. If the context is a list context, the item parameter is null, otherwise the current item is passed. The plug-in should return null if it can be used under the current circumstances, or a message explaining why not.

  4. After this, Plugin.done() is called and the plug-in instance is discarded. If there are several configurations for a plug-in, this procedure is repeated for each configuration.

Creating a new job

If the web client found that the plug-in could be used in a given context and the user selected the plug-in, the job configuration sequence is started. It is a wizard-like interface identical to the configuration wizard. In fact, the same JSP pages, and calling sequence is used. See the section called “Configuring a plug-in”. We do not repeat everything here. There are a few differences:

  • The job parameter is not null, but it does not contain any parameter values to start with. The plug-in should use this object to store job-related parameter values. The configuration parameter is null if the plug-in is started without configuration. In any case, the configuration values are write-protected and cannot be modified.

  • The first call to InteractivePlugin.getRequestInformation() is done with Request.COMMAND_CONFIGURE_JOB (_configjob) as the command. The context parameter reflects the current context.

  • When calling Response.setDone() the plug-in should use the variant that takes an estimated execution time. If the plug-in has support for immediate execution or download (export plug-ins only), it can also respond with Response.setExecuteImmediately() or Response.setDownloadImmediately().

    If the plug-in requested and was granted immediate execution or download the same plug-in instance is used to execute the plug-in. This may be done with the same or a new thread. Otherwise, a new job is added to the job queue, the parameter value are saved and the plug-in instance is discarded after calling the Plugin.done() method.

Executing a job

Normally, the creation of a job and the execution of it are two different events. The execution may as well be done on a different server. See Section 21.3, “Installing job agents”. This means that the execution takes place in a different instance of the plug-in class than what was used for creating the job. The exception is if a plug-in supports immediate execution or download. In this case the same instance is used, and it is, of course, always executed on the web server.

  1. A new instance of the plug-in class is created. The plug-in must have a public no-argument constructor.

  2. The Plugin.init() method is called. The job parameter contains the job configuration parameters. The configuration parameter is null if the plug-in does not have any configuration parameters.

  3. The Plugin.run() method is called. It is finally time for the plug-in to do the work it has been designed for. This method should not throw any exceptions. Use the Response.setDone() method to report success, the Response.setError() method to report errors or the Response.setContinue() method to respond to a shutdown signal and tell the core to resume the job once the system is up and running again. The Response.setContinue() can also be used when the system is not shutting down. The job will then be put back into the job queue and executed again later.

  4. In all cases the Plugin.done() method is called and the plug-in instance is discarded.

26.2.3. Abort a running a plug-in

BASE includes a simple signalling system that can be used to send signals to plug-ins. The system was primarly developed to allow a user to kill a plug-in when it is executing. Therfore, the focus of this chapter will be how to implement a plug-in to make it possible to kill it during it's execution.

Since we don't want to do this by brute force such as destroying the process or stopping thread the plug-in executes in, cooperation is needed by the plug-in. First, the plug-in must implement the SignalTarget interface. From this, a SignalHandler can be created. A plug-in may choose to implement it's own signal handler or use an existing implementation. BASE, for example, provides the ThreadSignalHandler implementation that supports the ABORT signal. This is a simple implementation that just calls Thread.interrupt() on the plug-in worker thread. This may cause two different effects:

  • The Thread.interrupted() flag is set. The plug-in must check this at regular intervals and if the flag is set it must cleanup, rollback open transactions and exit as soon as possible.

  • If the plug-in is waiting in a blocking call that is interruptable, for example Thread.sleep(), an InterruptedException is thrown. This should cause the same actions as if the flag was set to happen.

    [Warning] Not all blocking calls are interruptable

    For example calling InputStream.read() may leave the plug-in waiting in a non-interruptable state. In this case there is nothing BASE can do to wake it up again.

Example 26.10.  A plug-in that uses the ThreadSignalHandler


private ThreadSignalHandler signalHandler;
public SignalHandler getSignalHandler()
{
   signalHandler = new ThreadSignalHandler();
   return signalHandler;
}

public void run(Request request, Response response, ProgressReporter progress)
{
   if (signalHandler != null) signalHandler.setWorkerThread(null);
   beginTransaction();
   boolean done = false;
   boolean interrupted = false;
   while (!done && !interrupted)
   {
      try
      {
         done = doSomeWork(); // NOTE! This must not take forever!
         interrupted = Thread.interrupted();
      }
      catch (InterruptedException ex)
      {
         // NOTE! Try-catch is only needed if thread calls 
         // a blocking method that is interruptable 
         interrupted = true;
      }
   }
   if (interrupted)
   {
      rollbackTransaction();
      response.setError("Aborted by user", null);
   }
   else
   {
      commitTransaction();
      response.setDone("Done");
   }
}


Other signal handler implementations are ProgressReporterSignalHandler and EnhancedThreadSignalHandler. The latter handler also has support for the SHUTDOWN signal which is sent to plug-in when the system is shutting down. Clever plug-ins may use this to enable them to be restarted when the system is up and running again. See that javadoc for information about how to use it. For more information about the signalling system as a whole, see Section 29.3.10, “Sending signals (to plug-ins)”.

26.2.4. Using custom JSP pages for parameter input

This is an advanced option for plug-ins that require a different interface for specifying plug-in parameters than the default list showing one parameter at a time. This feature is used by setting the RequestInformation.getJspPage() property when constructing the request information object. If this property has a non-null value, the web client will send the browser to the specified JSP page instead of to the generic parameter input page.

When setting the JSP page you can either specify an absolute path or only the filename of the JSP file. If only the filename is specified, the JSP file is expected to be located in a special location, generated from the package name of your plug-in. If the plug-in is located in the package org.company the JSP file must be located in <base-dir>/www/plugins/org/company/.

An absolute path starts with '/' and may or may not include the root directory of the BASE installation. If, for example, BASE is intalled to http://your.base.server.com/base, the following absolute paths are equivalent /base/path/to/file.jsp, /path/to/file.jsp.

In both cases, please note that the browser still thinks that it is showing the regular parameter input page at the usual location: <base-dir>/www/common/plugin/index.jsp. All links in your JSP page should be relative to that directory.

Even if you use your own JSP page we recommend that you use the built-in facility for passing the parameters back to the plug-in. For this to work you must:

  • Generate the list of PluginParameter objects as usual.
  • Name all your input fields in the JSP like: parameter:name-of-parameter

    // Plug-in generate PluginParameter
    StringParameterType stringPT = new StringParameterType(255, null, true);
    PluginParameter one = new PluginParameter("one", "One", "First string", stringPT);
    PluginParameter two = new PluginParameter("two", "Two", "Second string", stringPT);
    
    // JSP should name fields as:
    First string: <input type="text" name="parameter:one"><br>
    Second string: <input type="text" name="parameter:two">
    
  • Send the form to index.jsp with the ID, cmd and requestId parameters as shown below.

    
    <form action="index.jsp" method="post">
    <input type="hidden" name="ID" value="<%=ID%>">
    <input type="hidden" name="requestId" value="<%=request.getParameter("requestId")%>">
    <input type="hidden" name="cmd" value="SetParameters">
    ...
    </form>
    
    

    The ID is the session ID for the logged in user and is required. The requestId is the ID for this particular plug-in/job configuration sequence. It is optional, but we recommend that you use it since it protects your plug-in from getting mixed up with other plug-in configuration wizards. The cmd=SetParameters tells BASE to send the parameters to the plug-in for validation and saving.

    Values are sent as strings to BASE that converts them to the proper value type before they are passed on to your plug-in. However, there is one case that can't be accurately represented with custom JSP pages, namely 'null' values. A null value is sent by not sending any value at all. This is not possible with a fixed form. It is of course possible to add some custom JavaScript that adds and removes form elements as needed, but it is also possible to let the empty string represent null. Just include a hidden parameter like this if you want an empty value for the 'one' parameter converted to null:

    <input type="hidden" name="parameter:one:emptyIsNull" value="1">
    

If you want a Cancel button to abort the configuration this should be linked to a page reload with with the url: index.jsp?ID=<%=ID%>&cmd=CancelWizard. This allows BASE to clean up resources that has been put in global session variables.

In your JSP page you will probably need to access some information like the SessionControl, Job and possible even the RequestInformation object created by your plug-in.

// Get session control and its ID (required to post to index.jsp)
final SessionControl sc = Base.getExistingSessionControl(pageContext, true);
final String ID = sc.getId();

// Get information about the current request to the plug-in
PluginConfigurationRequest pcRequest = 
   (PluginConfigurationRequest)sc.getSessionSetting("plugin.configure.request");
PluginDefinition plugin = 
   (PluginDefinition)sc.getSessionSetting("plugin.configure.plugin");
PluginConfiguration pluginConfig = 
   (PluginConfiguration)sc.getSessionSetting("plugin.configure.config");
PluginDefinition job = 
   (PluginDefinition)sc.getSessionSetting("plugin.configure.job");
RequestInformation ri = pcRequest.getRequestInformation();