13 min read

Atlassian JIRA, as we all know, is primarily an issue tracking and project management system. Since version 7.0, JIRA also comes in different flavors—namely JIRA CoreJIRA Software and JIRA Service Desk—each packaged to cater the needs of its various user categories. JIRA Core focuses on business teams, JIRA Software on software teams and JIRA Service desk on IT and service teams.

What many people do not know, though, is the power of its numerous customization capabilities, using which we can turn it into a different system altogether, much more powerful than these pre-packaged flavors! These extra capabilities can take JIRA to the next level, in addition to its core issue tracking and project tracking capabilities for which JIRA, arguably, is the best player in the market.

In this article by Jobin Kuruvilla, author of the book Jira 7 Development Cookbook – Third Edition, you will learn how to write a workflow condition and also the use of Active Objects to store data.

(For more resources related to this topic, see here.)

Writing a workflow condition

What are workflow conditions? They determine whether a workflow action is available or not. Considering the importance of a workflow in business processes and how there is a need to restrict the actions, either to a set of people (groups, roles, and so on) or based on some criteria (for example, the field is not empty), writing workflow conditions is almost inevitable.

Workflow conditions are created with the help of the workflow-condition module. The following are the key attributes and elements supported. Visit https://developer.atlassian.com/jiradev/jira-platform/building-jira-add-ons/jira-plugins2-overview/jira-plugin-module-types/workflow-plugin-modules#WorkflowPluginModules-Conditions for more details:

Attributes:

Name Description
key This should be unique within the plugin.
class Class to provide contexts for rendered velocity templates. It must implement the com.atlassian.jira.plugin.workflow.WorkflowPluginConditionFactory interface.
i18n-name-key The localization key for the human-readable name of the plugin module.
name Human-readable name of the workflow condition.

Elements:

Name Description
description Description of the workflow condition.
condition-class Class to determine whether the user can see the workflow transition. It must implement com.opensymphony.workflow.Condition. It is recommended to extend the com.atlassian.jira.workflow.condition.AbstractJiraCondition class.
resource type=”velocity” Velocity templates for the workflow condition views.

Getting ready

As usual, create a skeleton plugin. Then create an eclipse project using the skeleton plugin and we are good to go!

How to do it…

In this recipe, let’s assume we are going to develop a workflow condition that limits a transition only to the users belonging to a specific project role. The following are the steps to write our condition:

  1. Define the inputs needed to configure the workflow condition.We need to implement the WorkflowPluginFactory interface, which mainly exists to provide velocity parameters to the templates. It will be used to extract the input parameters that are used in defining the condition. To make it clear, the inputs here are not the inputs while performing the workflow action, but the inputs in defining the condition.The condition factory class, RoleConditionFactory in this case, extends the AbstractWorkflowPluginFactory, which implements the WorkflowPluginFactory interface. There are three abstract methods that we should implement, that is, getVelocityParamsForInput, getVelocityParamsForEdit, and getVelocityParamsForView. All of them, as the name suggests, are used for populating the velocity parameters for the different scenarios:

    In our example, we need to limit the workflow action to a certain project role, and so we need to select the project role while defining the condition. The three methods will be implemented as follows:

    private static final String ROLE = "role";
    
    private static final String ROLES = "roles";
    
    ...
    
    @Override
    
    protected void getVelocityParamsForEdit(Map<String, Object> velocityParams, AbstractDescriptor descriptor) {
    
        velocityParams.put(ROLE, getRole(descriptor));
    
        velocityParams.put(ROLES, getProjectRoles());
    
    }
    
    @Override
    
    protected void getVelocityParamsForInput(Map<String, Object> velocityParams){
    
        velocityParams.put(ROLES, getProjectRoles());
    
    }
    
     
    
    @Override
    
    protected void getVelocityParamsForView(Map<String, Object> velocityParams, AbstractDescriptor descriptor) {
    
        velocityParams.put(ROLE, getRole(descriptor));
    
    }

    Let’s look at the methods in detail:

    • getVelocityParamsForInput: This method defines the velocity parameters for input scenario, that is, when the user initially configures the workflow. In our example, we need to display all the project roles so that the user can select one to define the condition. The method getProjectRoles merely returns all the project roles and the collection of roles is then put into the velocity parameters with the key ROLES.
    • getVelocityParamsForView: This method defines the velocity parameters for the view scenario, that is, how the user sees the condition after it is configured. In our example, we have defined a role and so we should display it to the user after retrieving it back from the workflow descriptor. If you have noticed, the descriptor, which is an instance of AbstractDescriptor, is available as an argument in the method. All we need is to extract the role from the descriptor, which can be done as follows:
      private ProjectRole getRole(AbstractDescriptor descriptor) {
      
          if (!(descriptor instanceof ConditionDescriptor)){
      
              throw new IllegalArgumentException("Descriptor must be a ConditionDescriptor.");
      
          }
      
          ConditionDescriptor functionDescriptor = (ConditionDescriptor) descriptor;
      
          String role = (String) functionDescriptor.getArgs().get(ROLE);
      
          if (role != null && role.trim().length() > 0)
      
              return getProjectRole(role);
      
          else
      
             return null;
      
      }

    Just check if the descriptor is a condition descriptor or not, and then extract the role as shown in the preceding snippet.

    • getVelocityParamsForEdit: This method defines the velocity parameters for the edit scenario, that is, when the user modifies the existing condition. Here we need both the options and the selected value. So, we put both the project roles collection and the selected role on to the velocity parameters.
  2. The second step isto define the velocity templates for each of the three aforementioned scenarios:input, view, andedit. We can use the same template here for input and edit with a simple check to keep the old role selected for the edit scenario. Let us look at the templates:
    • role-condition-input.vm: Displays all project roles and highlights the already-selected one in the edit mode. In the input mode, the same template is reused, but the selected role will be null and so a null check is done:
      <tr>
      
      <td class="fieldLabelArea">Project Role: </td>
      
      <td nowrap>
      
      <select name="role" id="role">
      
            #foreach ($field in $roles)
      
      <option value="${field.id}"
      
      #if ($role && (${field.id}==${role.id})) SELECTED #end
      
      >$field.name</option>
      
                 #end
      
      </select>
      
      <br><font size="1">Select the role in which the user should be present!</font>
      
      </td>
      
      </tr>
    • role-condition.vm: Displays the selected role:
      #if ($role)
      
          User should have ${role.name} Role!
      
      #else
      
          Role Not Defined
      
      #end
  3. The third step is to write the actual condition. The condition class should extend theAbstractJiraConditionclass. Here, we need to implement the passesConditionmethod. In our case, we retrieve the project from the issue, check if the user has the appropriate project role, and return true if the user does:
    public boolean passesCondition(Map transientVars, Map args, PropertySet ps) {
    
        Issue issue = getIssue(transientVars);
    
        ApplicationUser user = getCallerUser(transientVars, args);
    
     
    
        Project project = issue.getProjectObject();
    
        String role = (String)args.get(ROLE);
    
        Long roleId = new Long(role);
    
     
    
        return projectRoleManager.isUserInProjectRole(user, projectRoleManager.getProjectRole(roleId), project);
    
    }

    The issue on which the condition is checked can be retrieved using thegetIssuemethod implemented in theAbstractJiraConditionclass. Similarly, the user can be retrieved using the getCallerUsermethod. In the preceding method, projectRoleManageris injected in the constructor, as we have seen before.

    Make sure you are using the appropriate scanner annotations for constructor injection, if the Atlassian Spring Scanner is defined in the pom.xml. See https://bitbucket.org/atlassian/atlassian-spring-scannerfor more details.

  4. We cansee that theROLEkey is used to retrieve the project role ID from the argsparameter in thepassesConditionmethod. In order for theROLEkey to be available in theargsmap, we need to override thegetDescriptorParamsmethod in the condition factory class,RoleConditionFactoryin this case. ThegetDescriptorParamsmethod returns a map of sanitized parameters, which will be passed into workflow plugin instances from the values in an array form submitted by velocity, given a set ofname:valueparameters from the plugin configuration page (that is, the input-parametersvelocity template). In our case, the method is overridden as follows:
    public Map<String, String> getDescriptorParams(Map<String, Object> conditionParams) {
    
        if (conditionParams != null && conditionParams.containsKey(ROLE)) {
    
            return MapBuilder.build(ROLE, extractSingleParam(conditionParams, ROLE));
    
        }
    
        // Create a 'hard coded' parameter
    
        return MapBuilder.emptyMap();
    
    }

    The method here builds a map of thekey:valuepair, where key isROLEand the value is the role value entered in the input configuration page. TheextractSingleParammethod is implemented in theAbstractWorkflowPluginFactoryclass. TheextractMultipleParamsmethod can be used if there is more than one parameter to be extracted!

  5. Allthat is left now is to populate theatlassian-plugin.xmlfile with the aforementioned components. We will use theworkflow-conditionmodule and it looks like the following block of code:
    <workflow-condition key="role-condition" name="Role Based Condition" i18n-name-key="role-condition.name" class="com.jtricks.jira.workflow.RoleConditionFactory">
    
    <description key="role-condition.description">Role Based Workflow Condition</description>
    
    <condition-class>com.jtricks.jira.workflow.RoleCondition</condition-class>
    
    <resource type="velocity" name="view" location="templates/conditions/role-condition.vm"/>
    
    <resource type="velocity" name="input-parameters" location="templates/conditions/role-condition-input.vm"/>
    
    <resource type="velocity" name="edit-parameters" location="templates/conditions/role-condition-input.vm"/>
    
    </workflow-condition>
  6. Packagethe plugin and deploy it!

How it works…

After the plugin is deployed, we need to modify the workflow to include the condition. The following screenshot is how the condition looks when it is added initially. This, as you now know, is rendered using the input template:

After the condition is added (that is, after selecting the Administratorsrole[N1] ), the view is rendered using the view template and looks as shown in the following screenshot:

If you try to edit it, the screen will be rendered using the same input template and the Administrators role, or whichever role was selected earlier, will be pre-selected.

After the workflow is configured, when the user goes to an issue, they will be presented with the transition only if they are a member of the project role where the issue belongs. It is while viewing the issue, that the passesCondition method in the condition class is executed.

Using Active Objects to store data

Active Objects represent a technology used by JIRA to allow per-plugin storage. This gives the plugin developers a real protected database where they can store the data belonging to their plugin and which other plugins won’t be able to access. In this recipe, we will see how we can store an address entity in the database using Active Objects.

You can read more about Active Objects at:

http://java.net/projects/activeobjects/pages/Home

Getting ready…

Create a skeleton plugin using the Atlassian Plugin SDK[SafisEd2] .

How to do it…

Following are the steps to use Active Objects in the plugin:

  1. Include the Active Objects dependency in pom.xml. Add the appropriate ao version, which you can find from the Active Objects JAR bundled in your JIRA:
    <dependency>
    
    <groupId>com.atlassian.activeobjects</groupId>
    
    <artifactId>activeobjects-plugin</artifactId>
    
    <version>${ao.version}</version>
    
    <scope>provided</scope>
    
    </dependency>
  2. Add the Active Objects plugin module to the Atlassian plugin descriptor:
    <ao key="ao-module">
    
    <description>The configuration of the Active Objects service</description>
    
    <entity>com.jtricks.entity.AddressEntity</entity>
    
    </ao>

    As you can see, the module has a unique key and it points to an entity we are going to define [SafisEd3] later, AddressEntity in this case.

  3. Include a component-import plugin to register ActiveObjects as a component in atlassian-plugin.xml:
    <component-import key="ao" name="Active Objects components" interface="com.atlassian.activeobjects.external.ActiveObjects">
    <description>Access to the Active Objects service</description>
    </component-import>

    Note that this step is not required if you are using the Atlassian Spring Scanner. Instead, you can use the @ComponentImport annotation, while injecting ActiveObjects in the constructor.

  4. Define the entity to be used for data storage. The entity should be an interface and should extend the net.java.ao.Entity interface. All we need to do in this entity interface is to define [SafisEd4] getter and setter methods for the data that we need to store for this entity.For example, we need to store the name, city, and country as part of the address entity. In this case, the AddressEntity interface will look like the following:
    public interface AddressEntity extends Entity {
    
     
    
        public String getName();
    
        public void setName(String name);
    
        public String getState();
    
        public void setState(String state);
    
        public String getCountry();
    
        public void setCountry(String country);
    
    }

    By doing this, we have set up the entity to facilitate the storage of all the three attributes.

    We can now create, modify, or delete the data using the ActiveObjects component. The component can be instantiated by injecting it into the constructor:

    private ActiveObjects ao;
    
     
    
    @Inject
    
    public ManageActiveObjects(@ComponentImport ActiveObjects ao) {
    
        this.ao = ao;
    
    }
  5. A new row can be added to the database using the following piece of code:
    AddressEntity addressEntity = ao.create(AddressEntity.class);
    
    addressEntity.setName(name);
    
    addressEntity.setState(state);
    
    addressEntity.setCountry(country);
    
    addressEntity.save();

Details can be read either using the ID, which is the primary key, or by querying the data using a net.java.ao.Queryobject. Using the ID is as simple as is shown in the following code line:

AddressEntity addressEntity = ao.get(AddressEntity.class, id);

The Query object can be used as follows:

AddressEntity[] addressEntities = ao.find(AddressEntity.class, Query.select().where("name = ?", name));

for (AddressEntity addressEntity : addressEntities) {

    System.out.println("Name:"+addressEntity.getName()+", State:"+addressEntity.getState()+", Country:"+addressEntity.getCountry());

}

Here, we are querying for all records with a given name.

Once you get hold of an entity by either means, we can edit the contents simply by using the setter method:

addressEntity.setState(newState);

addressEntity.save();

Deleting is even simpler!

ao.delete(addressEntity);

How it works…

Behind the scenes, separate tables are created in the JIRA database for every entity that we add. The Active Objects service interacts with these tables to do the work.

If you look at the database, a table of the name AO_{SOME_HEX}_MY_OBJECT is created for every entity named MyObject belonging to a plugin with the key com.example.ao.myplugin, where:

  • AO is a common prefix.
  • SOME_HEX is the set of the first six characters of the hexadecimal value of the hash of the plugin key com.example.ao.myplugin.
  • MY_OBJECT is the upper-case translation of the entity class name MyObject.

For every attribute with the getter method, getSomeAttribute, defined in the entity interface, a column is created in the table with the name SOME_ATTRIBUTE using the Java Beans naming convention—separating the two words by an underscore and keeping them both in upper case.

In our AddressEntity example, we have the following table, ao_a2a665_address_entity, as follows:

If you navigate to Administration | System | Advanced | Plugin Data Storage, you can find out all the tables created using Active Objects, as shown here:

As you can see, the table created using our example plugin is listed along with the tables created by other standard JIRA plugins.

Lots more about Active Objects can be read at:

https://developer.atlassian.com/docs/atlassian-platform-common-components/active-objects

Summary

In this article, just a couple of JIRA functionalities are explained. For more information you can refer to Jira 7 Development Cookbook, Third Edition. This book is your one-stop resource for mastering JIRA extension and customization. You will learn how to create your own JIRA plugins, customize the look-and-feel of your JIRA UI, work with workflows, issues, custom fields, and much more.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here