September 2004
[ vmassol ] 10:22, Thursday, 30 September 2004

On one of my projects at work we have moved to JIRA 3 (beta). We moved to benefit from the new custom workflow feature. Unfortunately it was missing one key feature we wanted: the ability to send notification emails on custom workflow transitions (I've just been told by Atlassian that this is a feature they're currently working on). To remedy this and thanks to Atlassian's support, I've decided to delve in the JIRA Java API and develop a workflow function plugin to implement email sending.

I have to say that JIRA 3's extensibility is great! JIRA can almost be seen as a full fledge foundation for developing project tracking applications, in the same spirit as Eclipse is a full fledge foundation for developing java applications (RCP). Both Eclipse and JIRA come with a default application using this API to demonstrate their power (the IDE for Eclipse, the issue tracker for JIRA). Note that the new plugin system in JIRA has several similiarities with the Eclipse plugin architecture. Of course, I'm sure JIRA still has a lot of ground to cover to expose a plugin API covering all domains of issue tracking (i.e. allowing to replace all parts of the JIRA issue tracker) but it's going in the right direction.

Here is a short tutorial on how to develop a workflow function plugin. Note that you should also check the Atlassian tutorial on how to develop plugins.

The source code is available here and the plugin jar is available here.

Setting up the project

Here's the directory structure I have chosen for my plugin. Please also note that I have used Maven to perform the build (extremely easy to setup as Atlassian is also using Maven and they have all their jars in a Maven remote repository on http://repository.atlassian.com).

A plugin is composed of several files (it is packaged as a JAR at runtime):

  • A plugin descriptor (atlassian-plugin.xml )
  • Java source files
  • Velocity templates for the plugin UI (the *.vm files)

The project.properties file simply adds the Atlassian Maven remote repo to the list of repos searched by Maven to download dependencies:

maven.repo.remote=http://repository.atlassian.com,http://www.ibiblio.org/maven

The project.xml contains the required JIRA dependencies and the definition of resources to include in the generated jar. Here's an extract:

<code>[...] <dependencies> <dependency> <groupId>atlassian-jira</groupId> <artifactId>atlassian-jira</artifactId> <version>3.0-beta</version> </dependency> <dependency> <groupId>osworkflow</groupId> <artifactId>osworkflow</artifactId> <version>17Aug2004</version> </dependency> <dependency> <groupId>propertyset</groupId> <artifactId>propertyset</artifactId> <version>1.3</version> </dependency> [...] <build> <sourceDirectory>src/main</sourceDirectory> <resources> <resource> <directory>src/etc</directory> <includes> <include>atlassian-plugin.xml</include> </includes> </resource> <resource> <directory>src/etc/templates</directory> <includes> <include>**/*.vm</include> </includes> </resource> </resources> </build> </code>

Generating the plugin jar is as simple as typing maven jar .

The Worflow Function plugin extension point

Here's what the atlassian-plugin.xml plugin descriptor contains:

<code><atlassian-plugin key="sendmail.jira.plugin.workflow.sendmail" name="SendMail Plugin"> <plugin-info> <description>Plugin for sending emails on custom workflow transitions.</description> <version>1.0</version> <application-version min="3.0" max="3.0"/> <vendor name="Vincent Massol" url="http://blogs.codehaus.org/people/vmassol/"/> </plugin-info> <workflow-function key="sendmail-function" name="Send Notification Mail" class="sendmail.jira.plugin.workflow.SendMailFunctionPluginFactory"> <description>Sends a notification email.</description> <function-class>sendmail.jira.plugin.workflow.SendMailFunction</function-class> <orderable>false</orderable> <unique>true</unique> <deletable>true</deletable> <weight>900</weight> <default>false</default> <resource type="velocity" name="view" location="sendmail-function-view.vm"/> <resource type="velocity" name="input-parameters" location="sendmail-function-input-params.vm"/> </workflow-function> </atlassian-plugin> </code>

What you have to understand:

  • A plugin is made of 2 java classes: a plugin factory class (SendMailFunctionPluginFactory ) which is in charge of setting up all that is necessary for the execution of the plugin feature, and the plugin execution class (SendMailFunction ).
  • A workflow plugin is expected to bundle 2 velocity template files: one for asking the user to input some data required by the plugin execution (this is the input-parametes velocity template, and one for displaying what the function will do. The later is visible if you click on a workflow transition in JIRA and then on the post-functions tab.

The Java API

The Plugin Factory class

Without further ado, here's the skeleton for the SendMailFunctionPluginFactory class:

public class SendMailFunctionPluginFactory extends AbstractWorkflowPluginFactory
    implements WorkflowPluginFunctionFactory
{
     public SendMailFunctionPluginFactory(FieldManager fieldManager)
     {
      }
 
     protected void getVelocityParamsForInput(Map velocityParams)
     {
      }
 
     protected void getVelocityParamsForView(Map velocityParams, 
          AbstractDescriptor descriptor)
     {
      }
 
     public Map getDescriptorParams(Map conditionParams)
     {
      }
}

Those 4 methods are called by JIRA itself:

  • The constructor is called when you click on the "add" button to add the function to your list of post-functions. The FieldManager instance can be used to get issue fields meta-data (it does not contain any issue data as there's no issue associated with the function yet - This will only happen when the function is triggered by an issue transition).
  • The getVelocityParamsForInput() method can be used to store some properties in the velocityParams map. These properties will then be accessible from the "input-parameters" Velocity template.
  • The getVelocityParamsForView() method can be used to store some properties in the velocityParams map. These properties will then be accessible from the "view" Velocity template. In addition the descriptor parameter provides access to the data entered by the user in the input phase (these data are stored in the workflow data structure itself).
  • The getDescriptorParams() method is the bridge between the data contained in the Velocity context and the data in the Workflow context. More precisely you put in there the code to extract the data that have been entered by the user in the Velocity context and you put the data in the workflow descriptor context. This descriptor context is the second parameter that is available in your getVelocityParamsForView() method.

Here's a look at the input-parameters velocity template:

<code><tr bgcolor=ffffff> <td align="right" valign="top" bgcolor="fffff0"> <span class="label">Group emails:</span> </td> <td bgcolor="ffffff" nowrap> <input type="text" name="groupEmails" value=""/> <br><font size="1">Comma-separated list of JIRA groups to send emails to.</font> </td> </tr> <tr bgcolor=ffffff> <td align="right" valign="top" bgcolor="fffff0"> <span class="label">Individual emails:</span> </td> <td bgcolor="ffffff" nowrap> <input type="text" name="individualEmails" value=""/> <br><font size="1">Comma-separated list of JIRA users to send emails to.</font> </td> </tr> </code>

As you can see, the variables groupEmails and individualEmails will hold the data entered by the user.

The Plugin Function class

Here's the code that implements the plugin feature (in our case the sending of the notification email):

public class SendMailFunction implements FunctionProvider
{
     public void execute(Map transientVars, Map args, PropertySet ps)
     {
      }
}

The execute() method is called by JIRA when an issue transition happens.

The parameters have the following meanings:

  • The transientVars parameter holds useful data such as the issue that was modified. You get a referernce to the issue by calling transientVars.get("issue"); . It contains also other piece of data such as the comment entered by the user, etc.
  • The args parameter holds all the data stored in the workflow context (aka the workflow descriptor). This is the data you have stored yourself in the getDescriptorParams() method explained above.
  • I'm not too sure what the ps parameter is used for. I think it holds data related to the workflow steps but this needs to be confirmed. Anyway, you shouldn't need it in most cases.

Deploying and executing the plugin

Deployment is a simple as dropping the plugin jar in [jira install dir]/atlassian-jira/WEB-INF/lib for example (it may be possible to drop it in other classloaders but I haven't tried).

In the following image we can see how JIRA has automatically discovered our plugin and extracted information from the plugin descriptor to make them available at the right extension point in JIRA:

Here is the page (using the input-parameters Velocity template) to let the user enter data for our function:

Here is how our function is displayed (using the view Velocity template):

I hope you got a good feel of what's possible to do with the JIRA API.

Note: For those wondering, I am not affiliated with Atlassian at all. I simply happen to like their tools (JIRA, Confluence) and I like the spirit of their team.