Tutorial - Creating workflow extensions

Applicable:

This tutorial applies to JIRA 5.0 and later.  

Level of experience:

This is an advanced tutorial. You should have completed at least one intermediate tutorial before working through this tutorial. See the list of tutorials in DAC.

Time estimate:

It should take you approximately 1 hour to complete this tutorial.

On this page:

Overview of the tutorial

JIRA administrators can customize JIRA workflows to suit the goals and processes of the teams that use a project. The workflow determines what states make up the JIRA issue lifecycle, along with rules for transitioning an issue from one state to another. Among other properties, the administrator can specify actions applicable to an issue at each state, who can transition an issue from one state to another, and any functions that are triggered by a workflow transition.

If you're not familiar with how to customize workflows in JIRA, see Configuring Workflow. It describes workflows from the JIRA administrator's perspective.

In this tutorial, you create a plugin that makes custom elements available for workflows. This tutorial is organized into three parts. Each part covers a particular type of workflow module, as follows:

  • Part 1 takes you through the steps to create a condition that prevents a subtask from being reopened if the parent task is resolved or closed.
  • Part 2 shows you how to add a function that closes the parent issue when the last subtask is closed.
  • Part 3 shows you how to add custom validation to your workflow.

The completed plugin will consist of the following components:

  • Java classes encapsulating the plugin logic.
  • Resources for display of the plugin user interface (UI).
  • A plugin descriptor (XML file) to enable the plugin module in the Atlassian application.

When you have finished, all these components will be packaged in a single JAR file.

About these Instructions

You can use any supported combination of OS and IDE to create this plugin. These instructions were written using IntelliJ IDEA on Mac OS X. If you are using another OS or IDE combination, you should use the equivalent operations for your specific environment.

This tutorial was last tested with JIRA 6.0.4.

Required knowledge

To complete this tutorial, you need to know the following:  

  • The basics of Java development: classes, interfaces, methods, how to use the compiler, and so on.
  • How to create an Atlassian plugin project using the Atlassian Plugin SDK.
  • JIRA administration, in particular, designing and administering project workflows. 

Plugin source

We encourage you to work through this tutorial. If you want to skip ahead or check your work when you have finished, you can find the plugin source code on Atlassian Bitbucket. Bitbucket serves a public Git repository containing the tutorial's code. To clone the repository, issue the following command:

git clone https://bitbucket.org/atlassian_tutorial/tutorial-jira-add-workflow-extensions

Alternatively, you can download the source as a ZIP archive by choosing download here: https://bitbucket.org/atlassian_tutorial/tutorial-jira-add-workflow-extensions. 

Part 1. Create the plugin project and the workflow condition module

First up, you will create a custom workflow condition. A condition prevents an issue from transitioning from one state to another based on a particular condition. In our case, the condition prevents a subtask from being reopened if the parent task is resolved or closed.

Step 1. Create the plugin project

In this step, you'll use the Atlassian Plugin SDK to generate the scaffolding for your plugin project. The Atlassian Plugin SDK automates much of the work of plugin development for you. It includes commands for creating a plugin and adding modules to the plugin.

  1. If you have not already set up the Atlassian Plugin SDK, do that now: Set up the Atlassian Plugin SDK and Build a Project.
  2. In the directory where you want to put the plugin project, enter the following SDK command:

    atlas-create-jira-plugin
    
  3. Choose 1 for JIRA 5 when asked which version of JIRA you want to create the plugin for.

  4. As prompted, enter the new plugin settings:

    group-id

    com.example.plugins.tutorial

    artifact-id

    add-workflow-extensions

    version

    1.0-SNAPSHOT

    package

    com.example.plugins.tutorial

  5. Confirm your entries when prompted.

The SDK generates the project home directory with project files, such as the POM (Project Object Model definition file), stub source code, and plugin resources.

Step 2. Tweak the POM

It's a good idea to familiarise yourself with the project configuration file, known as the POM (Project Object Model definition file). Among other functions, the POM (Project Object Model definition file) declares project dependencies and controls build settings. It also contains descriptive information for your plugin.

Tweak the metadata and add a dependency as follows:

  1. Open the POM (pom.xml) for editing. You can find it in the root folder of your project.
  2. Add your company or organization name and website URL to the organization element:

    <organization>
        <name>Example Company</name>
        <url>http://www.example.com/</url>
    </organization>
    
  3. Update the project description element to add a meaningful description, such as:

    <description>Extends JIRA issue reports.</description>
    

    The organization and description values you enter are propagated to the plugin descriptor file, atlassian.plugin.xml. From there, JIRA uses them in the administration console display for your plugin.

  4. Uncomment the jira-core dependency and change its scope to test:

      <dependency>
        <groupId>com.atlassian.jira</groupId>
        <artifactId>jira-core</artifactId>
        <version>${jira.version}</version>
        <scope>test</scope>
      </dependency>
    

    Although we discourage the use of JIRA core classes in your plugin, you'll need them for some testing code that the SDK will add for your module.

  5. Save the file.

Step 3. Add a plugin module to the project

Now you'll add a module to your plugin project. A module can be considered a unit of functionality in the Atlassian plugin framework. We'll add one that implements our custom workflow condition. Later you'll add modules for the post function and validator as well.

You can use the plugin module generator (another atlas command) to generate the stub code for modules needed by the plugin.

  1. Open a command window and go to the plugin root folder (where the pom.xml file is located).
  2. Run atlas-create-jira-plugin-module.

  3. Enter the number for the Workflow Condition module (currently 32). 
  4. Supply the following information as prompted:

    Enter New Classname

    ParentIssueBlockingCondition

    Package Name

    com.example.plugins.tutorial.jira.workflow

  5. Choose N for Show Advanced Setup.
  6. Choose N for Add Another Plugin Module.

The SDK generates starter Java files, template files, and test code for the module. It also modifies the POM and the plugin descriptor file, atlassian-plugin.xml, adding the workflow-condition module.

It's worth taking a look at the atlassian-plugin.xml file. This is an XML file that identifies the plugin to JIRA and defines the required plugin functionality. The file is located in your project under src/main/resources.

In the descriptor, you'll notice that the SDK added a workflow-condition module with three Velocity resources. The view resource uses the parent-issue-blocking-condition.vm template file, while the input-parameters and edit-parameters resources use parent-issue-blocking-condition-input.vm. Since the end user interaction between entering the initial configuration settings for a feature (the workflow condition, in our case) is the same thing as editing those settings, the assigned a single template to both resources. If your plugin called for it, you could define separate templates for these views.

We'll modify the templates next.

Step 4. Write the user interface

Our condition is going to be relatively straightforward. The parent issue needs to be in a particular state for the condition to be satisfied. Implement the logic for this in the templates as follows:

  1. Open the file src/main/resources/templates/conditions/parent-issue-blocking-condition.vm. Add a description of the intent of the condition, and then add logic for iterating through and displaying a list of statuses. These will be provided to the view by the Java classes you create later. Your template should look something like this:

    The parent issue must have one of the following statuses to allow sub-task transitions:
    #foreach ($status in $statuses)
        <b>$status.getName()</b>
        #if($velocityCount != $statuses.size())
            #if($velocityCount == ($statuses.size() - 1))
                &nbsp;or&nbsp;
            #else
                ,&nbsp;
            #end
        #else
            .
        #end
    #end
    

    The code is indented to make it easier to read, but as with all templates you may have to put this all on one line to avoid unwanted whitespace. As you can see, it simply loops around the provided Velocity parameter $statuses and prints a comma-separated list of each status.

  2. Now open the parent-issue-blocking-condition-input.vm file located in the same directory and add template code that allows users to choose the states that the parent issue must be in for the child issue to be closable, such as the following:

    <tr bgcolor="ffffff">
        <td align="right" valign="top" bgcolor="fffff0">
            <span class="label">Statuses:</span>
        </td>
        <td bgcolor="ffffff" nowrap>
            <table cellpadding="2" cellspacing="2">
            #foreach ($status in $statuses)
                <tr>
                    <td><input type="checkbox" name="$status.getId()"
                    #if (${selectedStatuses})
                        #if (${selectedStatuses.contains($status.getId())})
                        CHECKED
                        #end
                    #end
                    ></td>
                    <td>#displayConstantIcon ($status)&nbsp;$status.getName()</td>
                </tr>
            #end
            </table>
            <br><font size="1">The parent issue statuses required to allow sub-task issue transitions.</font>
        </td>
    </tr>
    

    Notice that we use the displayConstantIcon macro to display the status icon next to each status.

Step 5. Write the Java classes

So far, you have generated the stubs for your plugin modules and defined the views. Now make your code do something.

  1. Open the ParentIssueBlockingConditionFactory.java file for editing.  
    Notice that it contains methods that provide parameters to each of the views. For now, the parameter is a single Velocity parameter word with the default value test. We will modify the methods to do the following:
    • The getVelocityParamsForInput method should populate the Velocity parameter statuses with all available statuses.
    • The getVelocityParamsForView method should populate the Velocity parameter statuses with only those statuses that have been selected by the user. To do this, we retrieve the statuses argument from the ConditionDescriptor.
    • The getVelocityParamsForEdit method should populate the selectedStatuses Velocity parameter with the statuses stored in the ConditionDescriptor's statuses argument.
    • Finally, the getDescriptorParams method is used to retrieve the parameters in the ConditionDescription, so we will have it retrieve the statuses supplied in the input Map and store them in the statuses parameter. We use a comma-separated list, but you can use any mechanism to achieve this result you like.
  2. Replace the class with the following:

    public class ParentIssueBlockingConditionFactory extends AbstractWorkflowPluginFactory 
       implements WorkflowPluginConditionFactory
    {
        private final ConstantsManager constantsManager;
    
        public ParentIssueBlockingConditionFactory(ConstantsManager constantsManager)
        {
            this.constantsManager = constantsManager;
        }
    
        protected void getVelocityParamsForInput(Map velocityParams)
        {
            //all available statuses
            Collection<Status> statuses = constantsManager.getStatusObjects();
            velocityParams.put("statuses", Collections.unmodifiableCollection(statuses));
        }
    
        protected void getVelocityParamsForEdit(Map velocityParams, AbstractDescriptor descriptor)
        {
            getVelocityParamsForInput(velocityParams);
            velocityParams.put("selectedStatuses", getSelectedStatusIds(descriptor));
        }
    
        protected void getVelocityParamsForView(Map velocityParams, AbstractDescriptor descriptor)
        {
            Collection selectedStatusIds = getSelectedStatusIds(descriptor);
            List selectedStatuses = new LinkedList();
            for (Iterator iterator = selectedStatusIds.iterator(); iterator.hasNext();)
            {
                String statusId = (String) iterator.next();
                Status selectedStatus = constantsManager.getStatusObject(statusId);
                if (selectedStatus != null)
                {
                    selectedStatuses.add(selectedStatus);
                }
            }
            // Sort the list of statuses so as they are displayed consistently
            Collections.sort(selectedStatuses, new ConstantsComparator());
    
            velocityParams.put("statuses", Collections.unmodifiableCollection(selectedStatuses));
        }
    
        public Map getDescriptorParams(Map conditionParams)
        {
            //  process the map which will contain the request parameters
            //  for now simply concatenate into a comma separated string
            //  production code would do something more robust.
            Collection statusIds = conditionParams.keySet();
            StringBuffer statIds = new StringBuffer();
    
            for (Iterator iterator = statusIds.iterator(); iterator.hasNext();)
            {
                statIds.append((String) iterator.next() + ",");
            }
    
            return MapBuilder.build("statuses", statIds.substring(0, statIds.length() - 1));
        }
    
        private Collection getSelectedStatusIds(AbstractDescriptor descriptor)
        {
            Collection selectedStatusIds = new LinkedList();
            if (!(descriptor instanceof ConditionDescriptor))
            {
                throw new IllegalArgumentException("Descriptor must be a ConditionDescriptor.");
            }
    
            ConditionDescriptor conditionDescriptor = (ConditionDescriptor) descriptor;
    
            String statuses = (String) conditionDescriptor.getArgs().get("statuses");
            StringTokenizer st = new StringTokenizer(statuses, ",");
    
            while (st.hasMoreTokens())
            {
                selectedStatusIds.add(st.nextToken());
            }
            return selectedStatusIds;
        }
    }
    
  3. Add the import statements required by the new code. In general, you can use the import suggestions made by your IDE or you can refer to the source file on Bitbucket. You will likely get multiple suggestions from the IDE for the Status class. For that class, choose the following import:

    import com.atlassian.jira.issue.status.Status;
  4. Open the ParentIssueBlockingCondition.java file for editing. Notice that it extends AbstractJiraCondition and implements the passesCondition method. This method will contain the logic of the WorkflowCondition itself. It takes three arguments:
    • transientVars is a map of variables available for this method only. It is populated by JIRA and ensures that commonly needed variables are available, such as originalissueobject, which contains the IssueObject associated with the workflow.
    • args is a Map that contains values lifted straight from the form input and the ConditionDescriptor, so your "statuses" will be available from here.
    • ps contains properties that are defined in the workflow XML and persisted across workflow steps. In practice, this is rarely used.
  5. Replace the passesCondition method with the following:

    public boolean passesCondition(Map transientVars, Map args, PropertySet ps)
    {
        Issue subTask = (Issue) transientVars.get(WorkflowFunctionUtils.ORIGINAL_ISSUE_KEY);
    
        Issue parentIssue = ComponentAccessor.getIssueManager().getIssueObject(subTask.getParentId());
        if (parentIssue == null)
        {
            return false;
        }
    
        String statuses = (String) args.get("statuses");
        StringTokenizer st = new StringTokenizer(statuses, ",");
    
        while(st.hasMoreTokens())
        {
            String statusId = st.nextToken();
            if (parentIssue.getStatusObject().getId().equals(statusId))
            {
                return true;
            }
         }
         return false;
    }
    
  6. As before, add import statements by following the suggestions of your IDE or using the example on Bitbucket.

The logic for checking the condition is fairly straightforward: the method obtains the subtask issue from the transientVars map, obtains the list of allowable statuses from args, gets the parent issue from the IssueManager using getParentId(), and if the parent's status is one of the allowable statuses, returns true.

Step 6. Build, install and run the plugin

Let's start JIRA and see what we have so far. Before we can do that, however, we need to deal with the tests that the SDK gave us. As it stands, we've made enough changes to the source code that the assertions in the test code will give us errors.

The details of plugin testing are beyond the scope of this tutorial. But if you're already familiar with writing and running plugin tests, you can try updating the tests yourself. For a quicker alternative, try one of these options:

  • Replace your src/test directory with the equivalent test directory in the Bitbucket repository for this tutorial. Be sure to use the part1 branch, since the other branches have test code for classes you haven't added yet. You also need to add the following dependency to your POM:

    <dependency>
        <groupId>com.atlassian.jira</groupId>
        <artifactId>jira-tests</artifactId>
        <version>${jira.version}</version>
        <scope>test</scope>
    </dependency>
  • Run the atlas-run command mentioned below with the -DskipTests=true flag, which bypasses testing (although the tests are still compiled).
  • Alternatively, simply remove the src/test directory from your project for now.

To start JIRA with your plugin installed, follow these steps:

  1. Make sure you have saved all code changes to this point.
  2. Open a terminal window and navigate to the plugin root folder (where the pom.xml file is).
  3. Run the following command:

    atlas-run

    As a reminder, add -DskipTests=true to avoid running the test code for now.

    This command builds your plugin code, starts a JIRA instance, and installs your plugin. This may take a minute or two. When its done, you should see something like this towards the bottom of the screen print out:

    [INFO] [talledLocalContainer] Tomcat 6.x started on port [2990]
    [INFO] jira started successfully in 149s at http://atlas-laptop:2990/jira
    [INFO] Type Ctrl-D to shutdown gracefully
    [INFO] Type Ctrl-C to exit
  4. Open your browser and navigate to the JIRA URL indicated.
  5. At the login screen, enter the default username of admin with password admin.
  6. Create a project, as prompted, if this is your first time logging in. 

Next you will create the workflow and test your plugin.

Step 7. Customize a workflow and test the condition

The following steps describe how to apply your workflow condition in JIRA as a JIRA administrator. If you're not familiar with workflows or how administrators can customize and apply them, read more at Configuring Workflow in the JIRA documentation.

As an alternative to manually configuring your workflow, you can load a sample jirahome from the Bitbucket repository for this tutorial. Simply copy the src/test/resources/generated-test-resources.zip file from the repository to your project home, and add a productDataPath to your pom.xml:

<configuration>
   <productVersion>${jira.version}</productVersion>
   <productDataVersion>${jira.version}</productDataVersion>
   <productDataPath>${basedir}/src/test/resources/generated-test-resources.zip</productDataPath>
</configuration>

This was generated using the atlas-create-home-zip mechanism.

Here's the quick version:

  1. From anywhere in the JIRA interface, choose Issues from the cog menu and then Workflows from the left menu. 
  2. Click the Copy link next to the jira workflow. We'll work with a modified version of the default workflow. Alternatively, you could create a new one and start from scratch.
  3. Click the Reopen Issue transition from the Transitions column (in the text view).
  4. In the Conditions tab, click Add.
  5. Choose the Parent Issue Blocking Condition and then the Add button.
  6. In the Add Parameters To Condition page, choose Open and Reopened.
  7. Click the Workflow Schemes link in the left menu, and then Add Workflow Scheme.
  8. Type a name and description for the scheme and click Add.
  9. Click Add Workflow > Add Existing and choose the workflow you created. 
  10. Assign the workflow to the Sub-task issue type and click Finish
  11. Now, go back to your JIRA project and, in the administration page for the project, change the default workflow scheme to use your new workflow scheme. Follow the steps to associate and migrate the project to the new scheme.

That's it for the JIRA administration part. Now create a task and a subtask in your project and close both. Notice that you cannot reopen the subtask, as it's missing the Reopen button. It reappears if you reopen the parent task.

So far so good. Let's create our next module. From this point on, you can leave JIRA running and use FastDev or the atlas-cli with the plugin install (pi) SDK commands to reload your plugin changes on the fly. This saves the time of having to restart JIRA every time you change the plugin code or resource files.

Part 2. Create the workflow post function

Now create a workflow function that automatically closes the parent issue when all subtasks are closed. If you've cloned the Bitbucket repository for this tutorial, you can see the solution for this part by checking out at the part2 branch: 

git checkout part2

Step 1. Create the module

You can use the plugin module generator (another atlas command) to generate the stub code for modules needed by the plugin.

  1. At the command line, run the atlas-create-jira-plugin-module command from the project home.

  2. Enter the number for the Workflow Post Function module (currently 33). 
  3. Supply the following information as prompted:

    Enter New Classname

    CloseParentIssuePostFunction

    Package Name

    com.example.plugins.tutorial.jira.workflow

  4. Choose N for Show Advanced Setup.
  5. Choose N for Add Another Plugin Module.

As before this will generate boilerplate templates and factories, for you.

Step 2. Write the plugin

Now we'll make a few changes to the code

  1. Since we don't need a UI for this function, we'll remove those capabilities for our module:
    1. Open atlassian-plugin.xml and find the new workflow-function module.
    2. Change the class from com.example.plugins.tutorial.jira.workflow.CloseParentIssuePostFunctionFactory to com.atlassian.jira.plugin.workflow.WorkflowNoInputPluginFactory.
    3. Remove two resource elements, edit-parameters and input-parameters, from the same module declaration.
      That should leave you with a single resource declaration for the module, view, which points to the close-parent-issue-post-function.vm template.
    4. Save the file.
    5. Delete these files: 
      • CloseParentIssuePostFunctionFactory.java from src/main/java/com/example/plugins/tutorial/jira/workflow/
      • close-parent-issue-post-function-input.vm template from src/main/resources/templates/postfunctions/
  2. Open the close-parent-issue-post-function.vm template and replace its content with meaningful information about your function, such as:

    Parent Issue will be closed on closing final associated sub-task (all other associated sub-tasks are already closed).
    
  3. Open the CloseParentIssuePostFunction.java file for editing. It is located in src/main/java/com/example/plugins/tutorial/jira/workflow/.
  4. Add a few variable declarations and a constructor for our class:

        private final WorkflowManager workflowManager;
        private final SubTaskManager subTaskManager;
        private final JiraAuthenticationContext authenticationContext;
        private final Status closedStatus;
    
        public CloseParentIssuePostFunction(ConstantsManager constantsManager, WorkflowManager workflowManager, 
                                SubTaskManager subTaskManager, JiraAuthenticationContext authenticationContext) {
            this.workflowManager = workflowManager;
            this.subTaskManager = subTaskManager;
            this.authenticationContext = authenticationContext;
            closedStatus = constantsManager.getStatusObject(new Integer(IssueFieldConstants.CLOSED_STATUS_ID).toString());
        }
  5. Now implement the execute() method. This method does most of our work. Replace execute() with the following:

    public void execute(Map transientVars, Map args, PropertySet ps)throws WorkflowException{
    
        // Retrieve the sub-task
        MutableIssue subTask=getIssue(transientVars);
        // Retrieve the parent issue
        MutableIssue parentIssue = ComponentAccessor.getIssueManager().getIssueObject(subTask.getParentId());
    
        // Ensure that the parent issue is not already closed
        if (IssueFieldConstants.CLOSED_STATUS_ID == Integer.parseInt(parentIssue.getStatusObject().getId()))
        {
            return;
        }
    
        // Check that ALL OTHER sub-tasks are closed
        Collection<Issue> subTasks = subTaskManager.getSubTaskObjects(parentIssue);
    
        for (Iterator<Issue> iterator = subTasks.iterator(); iterator.hasNext();)
        {
            Issue associatedSubTask =  iterator.next();
            if (!subTask.getKey().equals(associatedSubTask.getKey()))
            {
                // If other associated sub-task is still open - do not continue
                if (IssueFieldConstants.CLOSED_STATUS_ID != 
                   Integer.parseInt(associatedSubTask.getStatusObject().getId()))
                {
                   return;
                }
            }
        }
    
        // All sub-tasks are now closed - close the parent issue
        try
        {
            closeIssue(parentIssue);
        }
        catch (WorkflowException e)
        {
            log.error("Error occurred while closing the issue: " + parentIssue.getString("key") + ": " + e, e);
            e.printStackTrace();
        }
    }
    

    Take a moment to look at the code. As you can see, it's fairly complex, as we need to interact with the workflow, interact with the IssueService in order to get the status of all the subtasks, and then, in our case, close the parent issues.
    Our function gets the subtask from the transientVars map, and then retrieves the parentIssue. If the parent issue is already closed, it simply returns. Next, it retrieves the subtasks using SubTaskManager. When writing your own post function, you can either inject this in the constructor (as here) or use ComponentAccessor to obtain an instance. If all the other subtasks are closed, close the parent issue. 

  6. Add the method we use to close the issue in our execute() method:

    private void closeIssue(Issue issue) throws WorkflowException
    {
        Status currentStatus = issue.getStatusObject();
        JiraWorkflow workflow = workflowManager.getWorkflow(issue);
        List<ActionDescriptor> actions = workflow.getLinkedStep(currentStatus).getActions();
        // look for the closed transition
        ActionDescriptor closeAction = null;
        for (ActionDescriptor descriptor : actions)
        {
            if (descriptor.getUnconditionalResult().getStatus().equals(closedStatus.getName()))
            {
                closeAction = descriptor;
                break;
            }
        }
        if (closeAction != null)
        {
            User currentUser =  authenticationContext.getLoggedInUser();
            IssueService issueService = ComponentAccessor.getIssueService();
            IssueInputParameters parameters = issueService.newIssueInputParameters();
            parameters.setRetainExistingValuesWhenParameterNotProvided(true);
            IssueService.TransitionValidationResult validationResult =  
                    issueService.validateTransition(currentUser, issue.getId(), 
                                                    closeAction.getId(), parameters);
            IssueService.IssueResult result = issueService.transition(currentUser, validationResult);
        }
    }


    Here we find the close transition using the WorkflowManager. When you have the transition, you can validate this transition using the IssueService, and then close the issue using the IssueService transition method.

  7. Add the import statements for the new code. As before, you can use the import suggestions made by your IDE or refer to the Bitbucket repository. For the Status and User classes, choose these packages to import:

    import com.atlassian.jira.issue.status.Status;
    import com.atlassian.crowd.embedded.api.User;
  8. Save the file.

Step 3. Test it

Now test that your plugin closes the parent task when all subtasks are closed: 

  1. Reload your plugin in JIRA, using FastDev, the pi command, or just by restarting JIRA.
  2. Go back to the workflow administration page, and edit your custom workflow.
  3. Add the Close Parent Issue Post Function to the close transition on your workflow. Note that there are multiple close transitions, so be sure to add the function to each one.
  4. Publish your workflow as modified.
  5. Back in your project, create a task with one or more subtasks.
  6. Close all the subtasks and make sure that the parent task has been closed by your post function.

Part 3. Create a validator

In this part of the tutorial you will add a validator that checks whether the user has entered a fixVersion. If not, the transition fails. The finished code for this part is in the part3 branch of the Bitbucket repository for this tutorial.

Step 1. Create the module

  1. Again, run the atlas-create-jira-plugin-module command from the command line.

  2. Enter the number for the Workflow Validator module. 
  3. Supply the following information as prompted:

    Enter New Classname

    CloseIssueWorkflowValidator

    Package Name

    com.example.plugins.tutorial.jira.workflow

  4. Choose N for Show Advanced Setup.
  5. Choose N for Add Another Plugin Module.

Step 2. Edit the code

Like the post function, this module does not need configuration screens. So first you'll remove the UI parts of the plugin, and then write the validator logic. 

  1. Either remove the validator factory that the plugin created, CloseIssueWorkflowValidatorFactory.java, or simply edit the class so that it does nothing. To make it do nothing, for example, replace its content with the following:

    package com.example.plugins.tutorial.jira.workflow;
    
    import com.atlassian.jira.plugin.workflow.AbstractWorkflowPluginFactory;
    import com.atlassian.jira.plugin.workflow.WorkflowPluginValidatorFactory;
    import com.google.common.collect.Maps;
    import com.opensymphony.workflow.loader.AbstractDescriptor;
    
    import java.util.Map;
    
    public class CloseIssueWorkflowValidatorFactory extends AbstractWorkflowPluginFactory implements WorkflowPluginValidatorFactory
    {
        public static final String FIELD_WORD="word";
    
        protected void getVelocityParamsForInput(Map velocityParams)
        {
        }
    
        protected void getVelocityParamsForEdit(Map velocityParams, AbstractDescriptor descriptor)
        {
        }
    
        protected void getVelocityParamsForView(Map velocityParams, AbstractDescriptor descriptor)
        {
        }
    
        public Map getDescriptorParams(Map validatorParams)
        {
            return Maps.newHashMap();
        }
    }
    
  2. Open CloseIssueWorkflowValidator.java and replace the validate() method with the following. This code is pretty simple; it simply checks that the issue has a fixVersion. If not, it throws an InvalidInputException.

        public void validate(Map transientVars, Map args, PropertySet ps) throws InvalidInputException
        {
            Issue issue = (Issue) transientVars.get("issue");
            // The issue must have a fixVersion otherwise you cannot close it
            if(null == issue.getFixVersions() || issue.getFixVersions().size() == 0)
            {
                throw new InvalidInputException("Issue must have a fix version");
            }
        }
    
  3. Edit the close-issue-workflow-validator.vm resource by replacing its content with a descriptive sentence or two, such as:

    The parent issue must have one of the following statuses to allow sub-task transitions:
    #foreach ($status in $statuses)<b>$status.getName()</b>#if($velocityCount != $statuses.size())#if($velocityCount == ($statuses.size() - 1))&nbsp;or&nbsp;#else,&nbsp;#end#else.#end#end
    
  4. While it won't get called, you can delete the close-issue-workflow-validator-input.vm template.

  5. In atlassian-plugin.xml, edit the resource declarations in the new workflow-validator element to have their location values refer to the remaining template, close-issue-workflow-validator.vm.

Step 3. Test the finished product

One more time:

  1. Reload your plugin using FastDev or the pi command.
  2. Once again, edit your workflow, this time to have the close issue transitions use the new Close Issue Workflow Validator you just created.
  3. Back in the project administration pages, create a version for your project.
  4. Now try to close a subtask that does not have a fix version. You should get a validation error.
  5. Give your subtask a version, and see if you can close it now.

Success!

Congratulations, that's it

Have a chocolate!

Was this page helpful?

Have a question about this article?

See questions about this article

Powered by Confluence and Scroll Viewport