19 min read

(For more resources on this topic, see here.)

Introduction

Workflows are one standout feature which help users to transform JIRA into a user-friendly system. It helps users to define a lifecycle for the issues, depending on the issue type, the purpose for which they are using JIRA, and so on. As the Atlassian documentation says at http://confluence.atlassian.com/display/JIRA/Configuring+Workflow:

A JIRA workflow is the set of steps and transitions an issue goes through during its lifecycle. Workflows typically represent business processes.

JIRA uses Opensymphony’s OSWorkflow which is highly configurable, and more importantly pluggable, to cater for the various requirements. JIRA uses three different plugin modules to add extra functionalities into its workflow, which we will see in detail through this chapter.
To make things easier, JIRA ships with a default workflow. We can’t modify the default workflow, but can copy it into a new workflow and amend it to suit our needs. Before we go into the development aspect of a workflow, it makes sense to understand the various components of a workflow.
The two most important components of a JIRA workflow are Step and Transition. At any point of time, an Issue will be in a step. Each step in the workflow is linked to a workflow Status (http://confluence.atlassian.com/display/JIRA/Defining+%27Status%27+F ield+Values) and it is this status that you will see on the issue at every stage. A transition, on the other hand, is a link between two steps. It allows the user to move an issue from one step to another (which essentially moves the issue from one status to another).
Few key points to remember or understand about a workflow:

  • An issue can exist in only one step at any point in time
  • A status can be mapped to only one step in the workflow
  • A transition is always one-way. So if you need to go back to the previous step, you need a different transition
  • A transition can optionally specify a screen to be presented to the user with the right fields on it

OSWorkflow, and hence JIRA, provides us with the option of adding various elements into a workflow transition which can be summarized as follows:

  • Conditions: A set of conditions that need to be satisfied before the user can actually see the workflow action (transition) on the issue
  • Validators: A set of validators which can be used to validate the user input before moving to the destination step
  • Post Functions: A set of actions which will be performed after the issue is successfully moved to the destination step

These three elements give us the flexibility of handling the various use cases when an issue is moved from one status to another. JIRA ships with a few built-in conditions, validators, and post functions. There are plugins out there which also provide a wide variety of useful workflow elements. And if you still don’t find the one you are looking for, JIRA lets us write them as plugins. We will see how to do it in the various recipes in this chapter. Hopefully, that gives you a fair idea about the various workflow elements. A lot more on JIRA workflows can be found in the JIRA documentation at http://confluence.atlassian.com/display/JIRA/Configuring+Workflow.

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 installations and how there is a need to restrict the actions either to a set of people, roles, and so on, or based on some criteria (for example, the field is not empty!), writing workflow conditions is inevitable.
Workflow conditions are created with the help of the workflow-condition module. The following are the key attributes and elements supported. See http://confluence.atlassian.com/display/JIRADEV/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. 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. Must implement com.opensymphony.workflow.Condition. 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. 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_NAME = "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. Hence, we put both the project roles collection and the selected role on to the velocity parameters.
  2. The second step is to define the velocity templates for each of the three aforementioned scenarios: input, view, and edit. 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:
    • edit-roleCondition.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 hence a null check is done:

      <tr bgcolor="#ffffff">
          <td align="right" valign="top" bgcolor="#fffff0">
              <span class="label">Project Role:</span>
          </td>
          <td bgcolor="#ffffff" 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>

    • view-roleCondition.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 the AbstractJiraCondition class. Here we need to implement the passesCondition method. 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) throws WorkflowException {
        Issue issue = getIssue(transientVars);
        User user = getCaller(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 the getIssue method implemented in the AbstractJiraCondition class. Similarly, the user can be retrieved using the getCaller method. In the preceding method, projectRoleManager is injected in the constructor, as we have seen before.

  4. We can see that the ROLE key is used to retrieve the project role ID from the args parameter in the passesCondition method. In order for the ROLE key to be available in the args map, we need to override the getDescriptorParams method in the condition factory class, RoleConditionFactory in this case. The getDescriptorParams method 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 of name:value parameters from the plugin configuration page (that is, the ‘input-parameters’ velocity 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 EasyMap.build(ROLE,
    extractSingleParam(conditionParams, ROLE));
            }
            // Create a 'hard coded' parameter
            return EasyMap.build();
      }

    The method here builds a map of the key:value pair, where key is ROLE and the value is the role value entered in the input configuration page. The extractSingleParam method is implemented in the AbstractWorkflowPluginFactory class. The extractMultipleParams method can be used if there is more than one parameter to be extracted!

  5. All that is left now is to populate the atlassian-plugin.xml file with the aforementioned components. We use the workflow-condition module and it looks like the following block of code:

    <workflow-condition key="role-condition" name="Role Based
    Condition"  class="com.jtricks.RoleConditionFactory">
        <description>Role Based Workflow Condition</description>
        <condition-class>com.jtricks.RoleCondition</condition-class>
        <resource type="velocity" name="view"
    location="templates/com/jtricks/view-roleCondition.vm"/>
        <resource type="velocity" name="input-parameters"
    location="templates/com/jtricks/edit-roleCondition.vm"/>
        <resource type="velocity" name="edit-parameters"
    location="templates/com/jtricks/edit-roleCondition.vm"/>
    </workflow-condition>

  6. Package the 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 Developers role), the view is rendered using the view template and looks as shown in the following screenshot:

(Move the mouse over the image to enlarge.)

If you try to edit it, the screen will be rendered using the edit template, as shown in the following screenshot:

Note that the Developers role is already selected.
After the workflow is configured, when the user goes to an issue, he/she will be presented with the transition only if he/she is 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.

Writing a workflow validator

Workflow validators are specific validators that check whether some pre-defined constraints are satisfied or not while progressing on a workflow. The constraints are configured in the workflow and the user will get an error if some of them are not satisfied. A typical example would be to check if a particular field is present or not before the issue is moved to a different status.
Workflow validators are created with the help of the workflow- validator module. The following are the key attributes and elements supported.

Attributes:

Name

Description

key

This should be unique within the plugin.

class

Class to provide contexts for rendered velocity templates. Must implement the com.atlassian.jira.plugin.workflow.WorkflowPluginValidatorFactory interface.

i18n-name-key

The localization key for the human-readable name of the plugin module.

name

Human-readable name of the workflow validator.

Elements:

Name

Description

description

Description of the workflow validator.

validator-class

Class which does the validation. Must implement com.opensymphony.workflow.Validator.

resource type=”velocity”

Velocity templates for the workflow validator views.

See http://confluence.atlassian.com/display/JIRADEV/Workflow+Plugin+Modules#WorkflowPluginModules-Validators for more details.

Getting ready

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

How to do it…

Let us consider writing a validator that checks whether a particular field has a value entered on the issue or not! We can do this using the following steps:

  1. Define the inputs needed to configure the workflow validator:
    We need to implement the WorkflowPluginValidatorFactory 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 validator. To make it clear, the inputs here are not the input while performing the workflow action, but the inputs in defining the validator.
    The validator factory class, FieldValidatorFactory in this case, extends the AbstractWorkflowPluginFactory interface and implements the WorkflowPluginValidatorFactory interface. Just like conditions, there are three abstract methods that we should implement. They are getVelocityParamsForInput, getVelocityParamsForEdit, and getVelocityParamsForView. All of them, as the names suggest, are used for populating the velocity parameters in different scenarios.
    In our example, we have a single input field, which is the name of a custom field. The three methods will be implemented as follows:

    @Override
    protected void getVelocityParamsForEdit(Map velocityParams,
    AbstractDescriptor descriptor) {
        velocityParams.put(FIELD_NAME, getFieldName(descriptor));
      velocityParams.put(FIELDS, getCFFields());
    } @Override
    protected void getVelocityParamsForInput(Map velocityParams) {
        velocityParams.put(FIELDS, getCFFields());
    } @Override
    protected void getVelocityParamsForView(Map velocityParams,
    AbstractDescriptor descriptor) {
        velocityParams.put(FIELD_NAME, getFieldName(descriptor));
    }

    You may have noticed that the methods look quite similar to the ones in a workflow condition, except for the business logic! Let us 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 custom fields, so that the user can select one to use in the validator. The method getCFFields returns all the custom fields and the collection of fields is then put into the velocity parameters with the key fields.
    • getVelocityParamsForView: This method defines the velocity parameters for the view scenario, that is, how the user sees the validator after it is configured. In our example, we have defined a field and so we should display it to the user after retrieving it back from the workflow descriptor. You may have noticed that the descriptor, which is an instance of AbstractDescriptor, is available as an argument in the method. All we need is to extract the field name from the descriptor, which can be done as follows:

      private String getFieldName(AbstractDescriptor descriptor){
        if (!(descriptor instanceof ValidatorDescriptor)) {
          throw new IllegalArgumentException('Descriptor must be a
      ValidatorDescriptor.');
        }
       
        ValidatorDescriptor validatorDescriptor = (ValidatorDescriptor)
      descriptor;   String field = (String)
      validatorDescriptor.getArgs().get(FIELD_NAME);
        if (field != null && field.trim().length() > 0)
          return field;
        else
          return NOT_DEFINED;
      }

      Just check if the descriptor is a validator descriptor or not and then extract the field 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 validator. Here we need both the options and the selected value. Hence we put both the custom fields’ collection and the field name onto the velocity parameters.
  2. The second step is to define the velocity templates for each of the three aforementioned scenarios, namely, input, view, and edit. We can use the same template here for input and edit with a simple checking to keep the old field selected for the edit scenario. Let us look at the template:
    • edit-fieldValidator.vm: Displays all custom fields and highlights the already selected one in edit mode. In input mode, the field variable will be null, and so nothing is pre-selected:

      <tr bgcolor="#ffffff">
        <td align="right" valign="top" bgcolor="#fffff0">
          <span class="label">Custom Fields :</span>
        </td>
        <td bgcolor="#ffffff" nowrap>
          <select name="field" id="field">
          #foreach ($cf in $fields)
            <option value="$cf.name"
              #if ($cf.name.equals($field)) SELECTED #end
            >$cf.name</option>
          #end
          </select>
          <br><font size="1">Select the Custom Field to be validated
      for NULL</font>
        </td>
      </tr>

    • view-fieldValidator.vm: Displays the selected field:

      #if ($field)
        Field '$field' is Required!
      #end

  3. The third step is to write the actual validator. The validator class should implement the Validator interface. All we need here is to implement the validate method. In our example, we retrieve the custom field value from the issue and throw an InvalidInputException if the value is null (empty):

    public void validate(Map transientVars, Map args, PropertySet ps)
    throws InvalidInputException, WorkflowException {
        Issue issue = (Issue) transientVars.get("issue");
        String field = (String) args.get(FIELD_NAME);      CustomField customField =
    customFieldManager.getCustomFieldObjectByName(field);     if (customField!=null){
          //Check if the custom field value is NULL
          if (issue.getCustomFieldValue(customField) == null){
            throw new InvalidInputException("The field:"+field+" is
                 required!"); }
        }
      }

    The issue on which the validation is done can be retrieved from the transientVars map. customFieldManager is injected in the constructor as usual.

  4. All that is left now is to populate the atlassian-plugin.xml file with these components. We use the workflow-validator module, and it looks like the following block of code:

    <workflow-validator key="field-validator" name="Field Validator" 
    class="com.jtricks.FieldValidatorFactory">
        <description>Field Not Empty Workflow Validator</description>     <validator-class>com.jtricks.FieldValidator</validator-class>     <resource type="velocity" name="view"
    location="templates/com/jtricks/view-fieldValidator.vm"/>
        <resource type="velocity" name="input-parameters"
    location="templates/com/jtricks/edit-fieldValidator.vm"/>
        <resource type="velocity" name="edit-parameters"
    location="templates/com/jtricks/edit-fieldValidator.vm"/>
    </workflow-validator>

  5. Package the plugin and deploy it!

Note that we have stored the role name instead of the ID in the workflow, unlike what we did in the workflow condition. However, it is safe to use the ID because administrators can rename the roles, which would then need changes in the workflows.

How it works…

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

After the validator is added (after selecting the Test Number field), it is rendered using the view template and looks as follows:

If you try to edit it, the screen will be rendered using the edit template, as shown in the following screenshot:

Note that the Test Number field is already selected.
After the workflow is configured, when the user goes to an issue and tries to progress it, the validator will check if the Test Number field has a value or not. It is at this point that the validate method in the FieldValidator class is executed.
If the value is missing, you will see an error, as shown in the following screenshot:

LEAVE A REPLY

Please enter your comment!
Please enter your name here