Automation Rule Components

Applicable:

This tutorial applies to JIRA Service Desk Server 3.2 and later.

For Cloud-related information, see implementing automation actions.

Level of experience:

This is an advanced tutorial. You should have completed at least one intermediate tutorial before working through this tutorial. Check out this tutorial or this one, or see the full 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

This tutorial shows you how to extend automation in JIRA Service Desk with new rule components that you can build. You'll learn how to build each type of rule component (a 'when', 'if' and 'then'). Once you've built your new components, they will be available for usage from the automation rule builder.

Required knowledge

To get the most out of this tutorial, you should be familiar with: 

  • The basics of Java development, such as classes, interfaces, methods, and so on.
  • How to use and administer JIRA.

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/service-desk-automation-tutorial.git

Alternatively, you can download the source as a ZIP archive from the Downloads page.

But before we start, let's provide some background and talk a little about the feature we're extending.

What is automation?

In a nutshell, automation in JIRA Service Desk allows service desk users to automate repetitive tasks, as well as allowing them to avoid missing important events. To get a feel for how it works, check out the user guide.

Rules and rule components

Automation consists of automation rules that perform actions (e.g. alert agent) triggered when specific events occur (e.g. issue is created) only if conditions (e.g. issue is high priority) are met.

These 3 concepts are named "Automation rule components" and form a fully defined "Automation rule":

Rule Component Type Details Available in JIRA Service Desk
WHEN Allows you to specify when an Automation Rule should be kicked off (e.g. what's triggering its execution)

By default, JIRA Service Desk will ship several WHEN triggers for you to use, including:

  • Comment added to issue
  • Comment edited in issue
  • Issue is created
  • Issue resolution is changed
  • Status changed, when issue is transitioned to a different stage in the issue type's workflow
  • A linked issue is transitioned, on the same JIRA instance
  • Request participant added to issue
  • Organization added to issue
  • Approval required, when issue is transitioned to an approval stage in the workflow
  • SLA time remaining, select the SLA and goal status that triggers the event
IF Allows you to specify conditions to filter an Automation Rule execution depending if the condition is met or not.

By default, JIRA Service Desk will ship several IF conditions for you to use, depending on your selected WHEN trigger, including:

  • Issue matches a certain filter
  • User type is a customer or agent
  • Comment visibility is internal or external
  • Comment contains a key phrase
  • Comment is primary action and not the consequence of another action (for example, commenting as part of a workflow transition)
  • Resolution change is either set or cleared
  • Status change visible to customer
  • Link type matches a certain type of link (for example, is related to or blocks)
THEN Allows you to specify which action(s) should be performed when the Automation Rule runs and if the conditions are met.

By default, JIRA Service Desk will ship several THEN actions for you to use, including:

  • Transition issue to change its position in the workflow
  • Add comment, either internal or external
  • Alert user to prompt a specific user or users via an @mention
  • Edit request type to change the request type (Because request types are mapped to specific issue types, automation isn't able to change issue types. Be sure your request types are the same issue type before applying this rule)
  • Edit issue to select and change a field in your issue, such as assignee or priority (this affects fields that may not appear in each issue type)
  • Send email to create an email notification
  • Webhook to send a POST request (see our tutorial)

Here's what it looks like when you set up an automation rule:

After completing this tutorial, you'll be able to implement your own 'when', 'if' and 'then' rule components, enabling you to extend automation to do pretty much anything you can think of, be it SMS notifications after certain actions, or integrating with external systems via REST calls.

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 7.2.10 and Service Desk 3.2.10

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.

Atlassian Plugin SDK

You can do more than just create plugins with the plugin SDK. For more information, see Atlassian Plugin SDK.

  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 following information to identify your plugin:

    group-id

    com.atlassian.plugins.tutorial.servicedesk

    artifact-id

    servicedesk-automation-extension

    version

    1.0.0-SNAPSHOT

    package

    com.atlassian.plugins.tutorial.servicedesk

  5. Confirm your entries when prompted.

The SDK finishes up and generates a directory for you with the initial project files, including a POM (Project Object Model definition file), stub source code, and resources.

Step 2. Review and tweak the generated stub code

It's a good idea to familiarise yourself with the project configuration file, known as the POM (Project Object Model definition file). The POM defines general settings for the project, including project dependencies and build settings.

The SDK generates and maintains the POM on its own, for the most part. However, you do need to tweak some of the included metadata for your project by hand, as follows: 

  1. Change to the project directory created by the SDK and open the pom.xml file for editing.
  2. Add your company or organisation name and your website as the name and url values of the organization element:

    <organization>
        <name>Example Company</name>
        <url>http://www.example.com/</url>
    </organization>
    
  3. Update the description element:

    <description>Adds a new when, if and then rule component to JIRA Service Desk's automation feature.</description>
  4. Save the file.
  5. Now open the atlassian-plugin.xml file created in the src/main/resources directory and remove the <web-resource><component> and <component-import> elements. We don't need those, as we're just adding implementations of automation components. After you've done this, your atlassian-plugin.xml should look something like this: 

    <atlassian-plugin key="${project.groupId}.${project.artifactId}" name="${project.name}" plugins-version="2">
        <plugin-info>
            <description>${project.description}</description>
            <version>${project.version}</version>
            <vendor name="${project.organization.name}" url="${project.organization.url}" />
            <param name="plugin-icon">images/pluginIcon.png</param>
            <param name="plugin-logo">images/pluginLogo.png</param>
        </plugin-info>
    
        <!-- add our i18n resource -->
        <resource type="i18n" name="i18n" location="servicedesk-automation-extension"/>
        
    </atlassian-plugin>

Step 3. Set up Atlassian Spring Scanner

Atlassian Spring Scanner allows us to wire in OSGi dependencies via annotations, in a more convenient way than the old XML based configuration.

  1. Add the Atlassian Spring Scanner compile and runtime dependencies to the root pom.xml

    <dependency>
        <groupId>com.atlassian.plugin</groupId>
        <artifactId>atlassian-spring-scanner-annotation</artifactId>
        <version>${atlassian.spring.scanner.version}</version>
        <scope>provided</scope>
    </dependency>
  2. Don't forget to add in the version property: 

    <atlassian.spring.scanner.version>2.0.1</atlassian.spring.scanner.version>
  3. Add the Atlassian Spring Scanner Maven plugin to <build><plugins>

    <plugin>
        <groupId>com.atlassian.plugin</groupId>
        <artifactId>atlassian-spring-scanner-maven-plugin</artifactId>
        <version>${atlassian.spring.scanner.version}</version>
        <executions>
            <execution>
                <goals>
                    <goal>atlassian-spring-scanner</goal>
                </goals>
                <phase>process-classes</phase>
            </execution>
        </executions>
        <configuration>
            <verbose>true</verbose>
        </configuration>
    </plugin>
  4. Finally, we must tell Spring to scan for annotations by defining an application context. Create a file named spring.xml in src/main/resources/META-INF/spring, with the following contents: 

    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:atlassian-scanner="http://www.atlassian.com/schema/atlassian-scanner/2"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
            http://www.atlassian.com/schema/atlassian-scanner/2
            http://www.atlassian.com/schema/atlassian-scanner/2/atlassian-scanner.xsd">
        <atlassian-scanner:scan-indexes/>
    </beans>

That's it - you can now wire in OSGi dependencies using annotations. We'll be doing this in a later step.

Step 4. Create your 'when' handler

We're going to create a 'when' handler that listens for when the assignee of a Service Desk request changes. This will allow Service Desk users to create an automation rule that does something when an assignee is updated.

4.1: Define the module

Add the following to the atlassian-plugin.xml file you modified earlier:

<automation-rule-when-handler key="issue-assignee-changed-tutorial-when-handler" name="Issue assignee changed" name-i18n-key="tutorial.when.handler.issue.assignee.changed">
    <icon-class>bp-jira</icon-class>
    <provides>
        <provide>issue</provide>
        <provide>user</provide>
    </provides>
</automation-rule-when-handler>

There are a few different properties defined here. Here's what they represent:

Property Purpose
key
This is the unique module key of your when handler. This key cannot be the same as any other module, so it's a good idea to include your company name or some other differentiator as part of it.
name

The non-internationalised name of your when handler. The name is what's displayed to end users in the UI.

If there is no name-i18n-key defined, or if if the i18n property does not exist, the display name on the automation UI will fall back to what's defined here.

name-i18n-key
The i18n key for the name of your when handler. This key should refer to a property defined in some .properties file in src/main/resources/i18n.
icon-class

This defines which icon will be used to represent your when handler. To see which classes map to which icons, have a look at this page .

unique *

Determines whether multiple instances of the when handler can be added to a single rule or not. The default value is false.

* This property is only applicable in Service Desk Server 3.1.x and Service Desk Cloud 3.2.0-OD-06.

provides

This defines what known items of information this when handler will provide to ifs and thens when part of a rule. The possible options are:

  • issue - the JIRA issue will be passed to the ifs and thens.
  • user - the JIRA user will be passed to the ifs and thens.
  • comment - the comment triggering the when handler will be passed to the ifs and thens.

Subsequently, when defining ifs and thens, you can specify which of these items of information you require, using <requires>. This "provides" and "requires" relationship affects the creation of rules in two ways:

  • When starting with a fresh rule, when a user selects a when handler, ifs and thens that "require" things the when handler does not "provide" will not appear in the UI.
  • For an existing rule, if a when handler is changed to something that no longer satisfies the "requires" of the ifs and/or thens, a validation error is shown to the user.

In our tutorial example, we're saying that our when handler will provide a user (which will be the new assignee), and an issue (the issue whose assignee has changed).

provides_requires

Seeing as we just defined an i18n key for our when handler, now is a good time to define the actual property this refers to. Under src/main/resources, create a new directory named i18n. Now in this directory, create a file named servicedesk-automation-extension.properties, with the following contents:

tutorial.when.handler.issue.assignee.changed=Issue assignee changed

4.2: Define the event handler

So far all we've done is define some metadata for our when handler. We still need to write and wire up the code that actually checks whether the assignee of an issue has changed, and provide the user and issue if it has. To do this, we need to create a new class that implements EventWhenHandler. This interface defines the following contract:

public interface EventWhenHandler<T>
{
    Class<T> getEventClass();
    public List<RuleExecutionCommand> handleEvent(@Nonnull List<WhenHandlerContext> contexts, @Nonnull T event);
}

getEventClass()returns the type of JIRA event while handleEvent() is invoked whenever an event of that type takes place. As we want to check whether the assignee of an issue has changed, we're interested in IssueEvents, so let's create an implementation for that.

Firstly, we need to make the automation SPIs (such as EventWhenHandler) and APIs available to us by adding them as a Maven dependency in the <dependencies> section of the root pom.xml file. We also need to add some other dependencies we'll use when creating our when handler:

<dependency>
    <groupId>com.atlassian.servicedesk.plugins.automation</groupId>
    <artifactId>servicedesk-automation-api</artifactId>
    <version>${servicedesk.automation.version}</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>com.atlassian.servicedesk.plugins.automation</groupId>
    <artifactId>servicedesk-automation-spi</artifactId>
    <version>${servicedesk.automation.version}</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>${springframework.version}</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>com.atlassian.pocketknife</groupId>
    <artifactId>atlassian-pocketknife-api-commons-jira</artifactId>
    <version>${pocketknife.api.commons.version}</version>
    <scope>provided</scope>
</dependency>

Don't forget to to add the appropriate version numbers to <properties>:

<servicedesk.automation.version>2.2.7</servicedesk.automation.version>
<springframework.version>4.1.7.RELEASE</springframework.version>
<pocketknife.api.commons.version>0.21.1</pocketknife.api.commons.version>

Next, we need to import the exact interfaces from JIRA and the automation plugin that we're going to be using. We're going to follow the pattern of defining these in a single Java file. This file can be located in any package under src/main/java, and should contain the following:

package com.atlassian.plugins.tutorial.servicedesk.osgi;

import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;

/**
 * This class is used to replace <component-import /> declarations in the atlassian-plugin.xml.
 * This class will be scanned by the atlassian spring scanner at compile time.
 * There is no situations where you ever need to create this class, it's here purely so that all the component-imports
 * are in the one place and not scattered throughout the code.
 */
@SuppressWarnings("UnusedDeclaration")
public class GeneralOsgiImports
{
    /******************************
    // Automation Engine
    ******************************/
    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.whenhandler.WhenHandlerProjectContextService whenHandlerProjectContextService;
    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.whenhandler.WhenHandlerRunInContextService whenHandlerRunInContextService;

    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.message.helper.IssueMessageHelper issueMessageHelper;
    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.message.helper.CommentMessageHelper commentMessageHelper;
    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.message.helper.UserMessageHelper userMessageHelper;
    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.message.RuleMessageBuilderService ruleMessageBuilderService;
    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.error.IfConditionErrorHelper ifConditionErrorHelper;

    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.command.RuleExecutionCommandBuilderService ruleExecutionCommandBuilderService;

    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.configuration.ruleset.input.BuilderService builderService;

    /******************************
    // JIRA
    ******************************/
    @ComponentImport com.atlassian.jira.issue.IssueManager issueManager;
    @ComponentImport com.atlassian.jira.security.PermissionManager permissionManager;
    @ComponentImport com.atlassian.jira.user.util.UserManager userManager;

    private GeneralOsgiImports()
    {
        throw new Error("This class should not be instantiated");
    }
}

Finally, we need to OSGi import the automation SPI and API packages (and others required by the Spring Scanner) by adding <instructions> to the <configuration> of the maven-jira-plugin configuration in the root pom.xml:

<instructions>
    <Atlassian-Plugin-Key>
        com.atlassian.plugins.tutorial.servicedesk.servicedesk-automation-extension
    </Atlassian-Plugin-Key>
    <Bundle-SymbolicName>
        com.atlassian.plugins.tutorial.servicedesk.servicedesk-automation-extension
    </Bundle-SymbolicName>
    <Spring-Context>*</Spring-Context>
    <Export-Package>
        com.atlassian.plugins.tutorial.servicedesk
    </Export-Package>
    <Import-Package>
        com.atlassian.servicedesk.plugins.automation.api.*,
        com.atlassian.servicedesk.plugins.automation.spi.*,
        *
    </Import-Package>
</instructions>

The full <plugin> definition should look like this:

<plugin>
    <groupId>com.atlassian.maven.plugins</groupId>
    <artifactId>maven-jira-plugin</artifactId>
    <version>${amps.version}</version>
    <extensions>true</extensions>
    <configuration>
        <products>
            <product>
                <id>jira</id>
                <instanceId>jira</instanceId>
                <version>${jira.version}</version>
                <applications>
                    <application>
                        <applicationKey>jira-servicedesk</applicationKey>
                        <version>${jira.servicedesk.application.version}</version>
                    </application>
                </applications>
                <pluginArtifacts>
                    <!-- Uncomment to install TestKit backdoor in JIRA. -->
                    <!--
                        <pluginArtifact>
                            <groupId>com.atlassian.jira.tests</groupId>
                            <artifactId>jira-testkit-plugin</artifactId>
                            <version>${testkit.version}</version>
                        </pluginArtifact>
                    -->
                </pluginArtifacts>
            </product>
        </products>
        <systemProperties>
            <atlassian.dev.mode>false</atlassian.dev.mode>
        </systemProperties>
        <skipITs>true</skipITs>
        <instructions>
            <Atlassian-Plugin-Key>
                com.atlassian.plugins.tutorial.servicedesk.servicedesk-automation-extension
            </Atlassian-Plugin-Key>
            <Bundle-SymbolicName>
                com.atlassian.plugins.tutorial.servicedesk.servicedesk-automation-extension
            </Bundle-SymbolicName>
            <Spring-Context>*</Spring-Context>
            <Export-Package>
                com.atlassian.plugins.tutorial.servicedesk
            </Export-Package>
            <Import-Package>
                com.atlassian.servicedesk.plugins.automation.api.*,
                com.atlassian.servicedesk.plugins.automation.spi.*,
                *
            </Import-Package>
        </instructions>
    </configuration>
</plugin>

Now that we've got our Maven and OSGi dependencies sorted, we can write out our EventWhenHandler implementation for checking if the assignee of an issue has changed. Here's the full implementation of AssigneeChangedEventWhenHandler

package com.atlassian.plugins.tutorial.servicedesk.when;

import com.atlassian.jira.event.issue.IssueEvent;
import com.atlassian.jira.issue.Issue;
import com.atlassian.jira.issue.IssueFieldConstants;
import com.atlassian.jira.issue.IssueManager;
import com.atlassian.jira.issue.changehistory.ChangeHistory;
import com.atlassian.jira.issue.history.ChangeItemBean;
import com.atlassian.jira.user.ApplicationUser;
import com.atlassian.jira.user.util.UserManager;
import com.atlassian.servicedesk.plugins.automation.api.execution.command.RuleExecutionCommand;
import com.atlassian.servicedesk.plugins.automation.api.execution.command.RuleExecutionCommandBuilder;
import com.atlassian.servicedesk.plugins.automation.api.execution.command.RuleExecutionCommandBuilderService;
import com.atlassian.servicedesk.plugins.automation.api.execution.message.RuleMessage;
import com.atlassian.servicedesk.plugins.automation.api.execution.message.RuleMessageBuilder;
import com.atlassian.servicedesk.plugins.automation.api.execution.message.RuleMessageBuilderService;
import com.atlassian.servicedesk.plugins.automation.api.execution.message.helper.IssueMessageHelper;
import com.atlassian.servicedesk.plugins.automation.api.execution.whenhandler.WhenHandlerContext;
import com.atlassian.servicedesk.plugins.automation.spi.rulewhen.event.EventWhenHandler;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Fires off rules when an issue is created
 */
public final class AssigneeChangedEventWhenHandler implements EventWhenHandler<IssueEvent>
{
    private static final Logger LOG = LoggerFactory.getLogger(AssigneeChangedEventWhenHandler.class);

    @Autowired
    private IssueMessageHelper issueMessageHelper;
    @Autowired
    private RuleMessageBuilderService ruleMessageBuilderService;
    @Autowired
    private RuleExecutionCommandBuilderService ruleExecutionCommandBuilderService;
    @Autowired
    private ProjectAndUserChecker projectAndUserChecker;

    @Autowired
    private IssueManager issueManager;
    @Autowired
    private UserManager userManager;

    @Override
    public Class<IssueEvent> getEventClass()
    {
        return IssueEvent.class;
    }

    /**
     * This method is invoked whenever an IssueEvent is fired.
     *
     * @param contexts contains the context for each assignee changed when handler that is configured as part of a rule
     * @param event the event that was fired
     * @return a list of rule execution commands; a rule execution will be performed for each member of this list
     */
    @Override
    public List<RuleExecutionCommand> handleEvent(
            final @Nonnull List<WhenHandlerContext> contexts,
            final @Nonnull IssueEvent event)
    {
        // If the assignee of the issue has not changed, we don't want to trigger any rule executions.
        // We do this by returning an empty list.
        if (!hasAssigneeChanged(event))
        {
            return Collections.emptyList();
        }

        // Here, we create the message that will be passed to ifs and thens. This message contains any contextual
        // information they need to do their job.
        final RuleMessage messageForIfsAndThens = createRuleMessage(event);

        // Now we need to build up our list of rule execution commands. We create a rule execution for each provided
        // when handler context.
        final RuleExecutionCommandBuilder ruleExecutionStub = ruleExecutionCommandBuilderService.builder()
                .requestSynchronousExecution(false)
                .ruleMessage(messageForIfsAndThens);

        final List<RuleExecutionCommand> ruleExecutions = new ArrayList<RuleExecutionCommand>();
        for (final WhenHandlerContext context : contexts)
        {
            if (projectAndUserAllowed(context, event))
            {
                RuleExecutionCommand command = ruleExecutionStub.ruleReference(context.getRuleReference()).build();
                ruleExecutions.add(command);
            }
        }

        return ruleExecutions;
    }

    public boolean hasAssigneeChanged(final IssueEvent event)
    {
        ChangeItemBean changeItem = getChangeItem(IssueFieldConstants.ASSIGNEE, event);
        if (changeItem == null)
        {
            return false;
        }

        return !StringUtils.defaultString(changeItem.getFrom()).equals(StringUtils.defaultString(changeItem.getTo()));
    }

    /**
     * Returns the change item bean that is associated to this field name in the context of this event.
     * Returns null if the field has not changed in the context of this event or if the issue has just been created
     *
     * @param fieldName the field name to look for
     * @param event     the event
     * @return a ChangeItemBean or null.
     */
    protected ChangeItemBean getChangeItem(final String fieldName, final IssueEvent event)
    {
        if (event.getChangeLog() == null)
        {
            return null;
        }

        ChangeHistory history = new ChangeHistory(event.getChangeLog(), issueManager, userManager);

        for (ChangeItemBean changeItem : history.getChangeItemBeans())
        {
            if (changeItem.getField().equals(fieldName))
            {
                return changeItem;
            }
        }
        return null;
    }

    /**
     * The rule message is what is passed to the ifs and thens. When we defined our when handler module in
     * atlassian-plugin.xml, we stated that we provide both the issue and the user, so we need to populate the rule
     * message with both here.
     */
    private RuleMessage createRuleMessage(final IssueEvent event)
    {
        final RuleMessageBuilder builder = ruleMessageBuilderService.builder();

        populateBuilderWithIssue(builder, event);
        populateBuilderWithUser(builder, event);

        return builder.build();
    }

    private void populateBuilderWithIssue(final RuleMessageBuilder toPopulate, IssueEvent event)
    {
        issueMessageHelper.setIssueData(toPopulate, event.getIssue());
    }

    private void populateBuilderWithUser(final RuleMessageBuilder toPopulate, IssueEvent event)
    {
        final ApplicationUser user = event.getUser();
        if (user != null)
        {
            toPopulate.put("userKey", user.getKey());
        }
    }

    /**
     * Checks whether the project the when handler has been configured in is the same project that issue is in, and
     * also checks that the configured user has browse permissions for the issue.
     */
    private boolean projectAndUserAllowed(final @Nonnull WhenHandlerContext context, final @Nonnull IssueEvent event)
    {
        final Issue issue = event.getIssue();

        // check project context first
        if (!projectAndUserChecker.isApplicableProject(context, issue.getProjectObject()))
        {
            return false;
        }

        // check view permission
        if (!projectAndUserChecker.canBrowseIssue(context, issue))
        {
            return false;
        }

        return true;
    }
}

This class implements handleEvent(), which is invoked whenever an IssueEvent is fired. The method works out whether the issue assignee has changed. If it hasn't changed, it returns an empty list, indicating that the rule execution should not propagate further to ifs and actions. If it has changed, it does the following:

  1. Constructs a rule message. This message contains contextual information that is passed to ifs and thens. They need this information to do their job. When we defined the when handler module, we said that we provide issue and user, so we must add those to the rule message. 
  2. Builds up the list of rule executions that need to take place. A rule execution needs to take place for every rule that has this when handler configured. The rule message constructed previously is added to each rule execution. When building up this list, a check is made on each when handler configuration that's part of a rule to make sure it's for the correct project, and that the configured user has permissions to view the issue who's assignee has changed.
  3. Returns the list of rule executions. Each one of these is then executed, either asynchronously or on the same thread. Whether it's synchronous or not is defined by the requestSynchronousExecution(boolean) method of RuleExecutionCommandBuilder. In AssigneeChangedEventWhenHandler, we asked that each rule execution happen asynchronously:
final RuleExecutionCommandBuilder ruleExecutionStub = ruleExecutionCommandBuilderService.builder()
        .requestSynchronousExecution(false)
        .ruleMessage(messageForIfsAndThens);

AssigneeChangedEventWhenHandler makes use of numerous helper classes and services that are provided by the automation plugin. ProjectAndUserChecker is the exception, that's something that you need to write yourself. Here's the implementation we use in the tutorial:

package com.atlassian.plugins.tutorial.servicedesk.when;

import com.atlassian.fugue.Either;
import com.atlassian.jira.issue.Issue;
import com.atlassian.jira.permission.ProjectPermissions;
import com.atlassian.jira.project.Project;
import com.atlassian.jira.security.PermissionManager;
import com.atlassian.jira.user.ApplicationUser;
import com.atlassian.pocketknife.api.commons.error.AnError;
import com.atlassian.servicedesk.plugins.automation.api.execution.context.project.ProjectContext;
import com.atlassian.servicedesk.plugins.automation.api.execution.context.user.InContextFunction;
import com.atlassian.servicedesk.plugins.automation.api.execution.whenhandler.WhenHandlerContext;
import com.atlassian.servicedesk.plugins.automation.api.execution.whenhandler.WhenHandlerProjectContextService;
import com.atlassian.servicedesk.plugins.automation.api.execution.whenhandler.WhenHandlerRunInContextService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;
import java.util.List;

@Component
public class ProjectAndUserChecker
{
    private static final Logger LOG = LoggerFactory.getLogger(ProjectAndUserChecker.class);

    @Autowired
    private WhenHandlerProjectContextService whenHandlerProjectContextService;
    @Autowired
    private WhenHandlerRunInContextService whenHandlerRunInContextService;
    @Autowired
    private PermissionManager permissionManager;

    /**
     * Checks whether a when handler context matches a given project.
     */
    public boolean isApplicableProject(@Nonnull WhenHandlerContext context,
                                       @Nonnull Project project)
    {
        final Either<AnError, ProjectContext> applicationProjectContext = whenHandlerProjectContextService.getApplicationProjectContext(context);
        if (applicationProjectContext.isLeft())
        {
            LOG.debug("Unable to fetch project context for given when handler context: " + context.toString());
            return false;
        }

        final List<Project> projects = applicationProjectContext.right().get().getProjects();
        if (projects.isEmpty())
        {
            return true;
        }
        else
        {
            return projects.contains(project);
        }
    }


    /**
     * Can the passed user browse the given issue?
     */
    public boolean canBrowseIssue(@Nonnull WhenHandlerContext context,
                                  @Nonnull final Issue issue)
    {
        return whenHandlerRunInContextService.executeInContext(context, new InContextFunction<Boolean>()
        {
            @Override
            public Boolean run(final ApplicationUser user)
            {
                return permissionManager.hasPermission(ProjectPermissions.BROWSE_PROJECTS, issue, user);
            }
        });
    }
}

Now that we've defined our when handler classes, the final step is to add the event when handler definition to our atlassian-plugin.xml:

<automation-rule-event-when-handler key="issue-assignee-changed-tutorial-event-when-handler"
                                    class="com.atlassian.plugins.tutorial.servicedesk.when.AssigneeChangedEventWhenHandler">
    <automation-rule-when-handler module-key="issue-assignee-changed-tutorial-when-handler" />
</automation-rule-event-when-handler>

Note that you need this in addition to the when handler definition added earlier. Make sure the module-key value matches the 'key' of the when handler definition above. Your full atlassian-plugin.xml should now look like this:

<atlassian-plugin key="${project.groupId}.${project.artifactId}" name="${project.name}" plugins-version="2">
    <plugin-info>
        <description>${project.description}</description>
        <version>${project.version}</version>
        <vendor name="${project.organization.name}" url="${project.organization.url}" />
        <param name="plugin-icon">images/pluginIcon.png</param>
        <param name="plugin-logo">images/pluginLogo.png</param>
    </plugin-info>

    <!-- add our i18n resource -->
    <resource type="i18n" name="i18n" location="servicedesk-automation-extension"/>

    <automation-rule-when-handler key="issue-assignee-changed-tutorial-when-handler" name="Issue assignee changed" name-i18n-key="tutorial.when.handler.issue.assignee.changed">
        <icon-class>bp-jira</icon-class>
        <provides>
            <provide>issue</provide>
            <provide>user</provide>
        </provides>
    </automation-rule-when-handler>

    <automation-rule-event-when-handler key="issue-assignee-changed-tutorial-event-when-handler"
                                        class="com.atlassian.plugins.tutorial.servicedesk.when.AssigneeChangedEventWhenHandler">
        <automation-rule-when-handler module-key="issue-assignee-changed-tutorial-when-handler" />
    </automation-rule-event-when-handler>
    
</atlassian-plugin>

4.3: Test that your when handler is available to use

Follow steps 2-6 of the JIRA Service Desk plugin development flow. Once you've installed the plugin in your running JIRA instance, open the Settings tab on any Service Desk (create a Service Desk if one does not yet exist), and select 'Automation'.

Add an automation rule, using 'Custom rule' as the template. You should now see a new 'Issue assignee changed' option when setting up the WHEN of a rule.

That's sweet, but lets see if it actually works. Create a complete rule, with 'Issue assignee changed' as the trigger. Let's make it add a comment when this happens, for any issue:

Now save the rule and lets test it out. To actually change the assignee of an issue, you'll need to have at least two users in your JIRA instance, each of which has access to the project you created an automation rule for. You'll need to add the second user as an agent to the Service Desk via the 'People' tab.

Create or open an issue in same project you just set up the automation rule for. Now change the assignee. You should see a comment added automatically to the JIRA issue. Nice work!

Step 5. Create your 'if' condition

So now we can tell if the assignee of an issue has changed. Under which conditions do we want to perform an action when this happens? How about if the person that changed the assignee has an email address that belongs to a particular domain? This may be useful if, for example, you want to perform an action only if the user that changes the assignee is from an external company.

5.1: Define module

Add the following to the atlassian-plugin.xml file:

<automation-rule-if-condition key="user-email-domain-tutorial-if-condition" class="com.atlassian.plugins.tutorial.servicedesk.ruleif.UserEmailDomainIfCondition" name="User email domain"
                              name-i18n-key="tutorial.if.condition.user.email.domain.name">
    <icon-class>user</icon-class>
    <requires>
        <require>user</require>
    </requires>
    <visualiser class="com.atlassian.plugins.tutorial.servicedesk.ruleif.UserEmailDomainIfConditionVisualiser" />
    <validator class="com.atlassian.plugins.tutorial.servicedesk.ruleif.UserEmailDomainIfConditionValidator"/>
    <web-form-module>servicedesk/settings/automation/tutorial/modules/ruleif/useremaildomain-if-condition-form</web-form-module>
</automation-rule-if-condition>

Like our when handler module definition, we define a keynamename-i18n-key and icon-class properties. See "Define the module" in the when handler component section for more information on these. The following properties we haven't encountered before:

Property Purpose
requires

This is analogous to provides in our when handler definition, and defines the list of things we require for this rule component to be available for selection. To check the email address domain of the user that caused the rule to be executed, we only require the user, so that's all we define here.

See  "Define the module" in the when handler component section for the other things we could add here.

visualiser

By default, whatever label is defined by name (or the value of the i18n-name-key property) is displayed on the UI for the rule component. For example, our when handler has "Issue assignee changed" as its name, and this is how that looks in the UI:

However, what if we want this label to change based on the configuration of the component? In our case, it would make it far easier to read a rule if our label was "User email domain is 'atlassian.com'" rather than just "User email domain". This is what a visualiser does: it allows you to define how the rule component label should appear based on the configuration.

The visualiser class you create must implement the RuleComponentVisualiser interface, which defines a contract like the following:

public interface RuleComponentVisualiser 
{
 public String getName(@Nonnull RuleComponentVisualiserParam ruleComponentVisualiserParam);
 public Option<String> getLabel(@Nonnull RuleComponentVisualiserParam ruleComponentVisualiserParam);
 public static interface RuleComponentVisualiserParam
 { 
 public ApplicationUser getUser();
 public ConfigurationData ruleConfiguration();
 }
}

Defining a visualiser is optional. If you don't define one, or if the user has not yet added any configuration, name (or the value of the i18n-name-key property) is used.

validator

The validator you define here will be invoked for your component before a rule is saved and before a rule is loaded. If a failure result is returned, then either the entire rule will not be saved, or it will be loaded with an error highlighted for your component.

The validator class you create must implement the IfConditionValidator interface, which defines the following contract:

public interface IfConditionValidator
{
    ValidationResult validate(@Nonnull IfConditionValidationParam ifConditionValidationParam);

    public static interface IfConditionValidationParam
    {
        public ApplicationUser getUserToValidateWith();
        public IfConditionConfiguration getConfiguration();
        public Option<ProjectContext> getProjectContext();
    }
}

Defining a validator for your rule component is optional. If you don't define one, the component will always pass validation.

web-form-module

This is the AMD module which defines the front-end appearance and logic of your rule component. This module handles things like UI layout, client-side validation, and error message rendering.

We've defined an i18n key for our if condition, so we should also define the actual property this refers to. In the src/main/resources/i18n/servicedesk-automation-extension.properties file created earlier, add the following line:

tutorial.if.condition.user.email.domain.name=User email domain

5.2: Define your if condition

Now that we have our if condition metadata defined, let's implement the if condition class itself. As mentioned previously, this class need to implement the IfCondition interface. Here's the full implementation of UserEmailDomainIfCondition:

package com.atlassian.plugins.tutorial.servicedesk.ruleif;

import com.atlassian.fugue.Either;
import com.atlassian.fugue.Option;
import com.atlassian.jira.user.ApplicationUser;
import com.atlassian.pocketknife.api.commons.error.AnError;
import com.atlassian.servicedesk.plugins.automation.api.execution.error.IfConditionError;
import com.atlassian.servicedesk.plugins.automation.api.execution.error.IfConditionErrorHelper;
import com.atlassian.servicedesk.plugins.automation.api.execution.message.helper.UserMessageHelper;
import com.atlassian.servicedesk.plugins.automation.spi.ruleif.IfCondition;
import org.springframework.beans.factory.annotation.Autowired;

import static com.atlassian.fugue.Either.right;

/**
 * If condition that checks whether a user's email address belongs to a specified domain.
 *
 */
public final class UserEmailDomainIfCondition implements IfCondition
{
    private static final String EMAIL_DOMAIN_KEY = "emailDomain";

    private final UserMessageHelper userMessageHelper;
    private final IfConditionErrorHelper ifConditionErrorHelper;

    @Autowired
    public UserEmailDomainIfCondition(
            final UserMessageHelper userMessageHelper,
            final IfConditionErrorHelper ifConditionErrorHelper)
    {
        this.userMessageHelper = userMessageHelper;
        this.ifConditionErrorHelper = ifConditionErrorHelper;
    }

    /**
     * This method is invoked whenever a rule that contains a user email domain if condition is executed. 
     * If this method returns anything other than an {@code Either.right(true)}, then rule execution halts, and any 
     * then actions defined as part of the rule will not be invoked.
     * 
     * @param ifConditionParam contains all the contextual information required by the if condition to do its job. 
     * @return Either.left upon error, or an Either.right with a boolean indicating whether the condition has been met 
     *         or not
     */
    @Override
    public Either<IfConditionError, Boolean> matches(final IfConditionParam ifConditionParam)
    {
        // Get the email domain we want to check for
        final Option<String> emailDomainOpt = ifConditionParam.getConfiguration().getData().getValue(EMAIL_DOMAIN_KEY);
        if(emailDomainOpt.isEmpty())
        {
            return ifConditionErrorHelper.error("No " + EMAIL_DOMAIN_KEY + " property in config data");
        }
        final String emailDomainToCheckFor = emailDomainOpt.get();

        // Get the email domain of the user that initiated the rule
        final Either<AnError, ApplicationUser> userEither = userMessageHelper.getUser(ifConditionParam.getMessage(), UserMessageHelper.CURRENT_USER_USER_PREFIX);
        if (userEither.isLeft())
        {
            return ifConditionErrorHelper.error(userEither.left().get());
        }
        final ApplicationUser userToCheck = userEither.right().get();
        final String userEmailDomain = getEmailDomain(userToCheck);

        // Return the match result
        return right(userEmailDomain.equalsIgnoreCase(emailDomainToCheckFor));
    }

    private String getEmailDomain(final ApplicationUser fromUser)
    {
        return fromUser.getEmailAddress().substring(
                fromUser.getEmailAddress().indexOf('@') + 1
        );
    }
}

When a rule is executing that is configured with a user email domain if condition, the matches() method is invoked. The ifConditionParam contains any contextual information needed by the if condition to do its job, including whatever email domain has been configured to check for. The email domain is stored in the map of data contained in the ifConditionParam's data map. This map contains all the data configured for the user email domain if condition in a particular rule.

We retrieve the email domain value from the map using the "email.domain" key. This is what we'll store the value under when we define the front-end of our rule component.

Once we have the email domain to check for, the next thing our match() implementation does is retrieve the email address (and hence the domain) for the user that caused the initial rule invocation. If the rule has been configured with an assignee changed when handler, this will be the user that has changed the assignee. This user is stored in the provided rule message. To make it easier to extract this user, we use UserMessageHelper, a helper class provided by the automation API module.

Now we perform the simple check to see if the user's email address domain matches the expected domain that was configured, and return the result.

You'll notice in our UserEmailDomainIfCondition that we made use of an IfConditionErrorHelper. As this is something that's provided by another plugin, the last thing we need to do is add the following line to our GeneralOsgiImports file:

@ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.error.IfConditionErrorHelper ifConditionErrorHelper;

If we forget to do this, our if condition implementation will fail at runtime with a NullPointerException.

5.3: Define the visualiser

As mentioned earlier, the visualiser is used to define the logic that dictates what name and label are displayed for the rule component. This can be done dynamically, based on the rule component's configuration. The rule component name and label appear in the following places:

To do this, we create a class that implements RuleComponentVisualiser, and add the logic to return the name and the label when given the configuration. Here's a full implementation for our user email domain example:

package com.atlassian.plugins.tutorial.servicedesk.ruleif;

import com.atlassian.fugue.Option;
import com.atlassian.jira.util.I18nHelper;
import com.atlassian.servicedesk.plugins.automation.spi.visualiser.RuleComponentVisualiser;

import javax.annotation.Nonnull;

import static com.atlassian.fugue.Option.none;
import static com.atlassian.fugue.Option.some;

/**
 * This visualiser is responsible for deciding what name and label to show for user email domain if condition rule
 * components. The name never changes, but the label displayed will show the email domain, if this has been configured.
 *
 */
public final class UserEmailDomainIfConditionVisualiser implements RuleComponentVisualiser
{
    private final I18nHelper i18nHelper;

    @Autowired
    public UserEmailDomainIfConditionVisualiser(final I18nHelper i18nHelper)
    {
        this.i18nHelper = i18nHelper;
    }

    /**
     * Returns the name to use for this if condition rule component. The name appears above the label, adjacent to the
     * rule component icon.
     */
    @Nonnull
    @Override
    public String getName(final RuleComponentVisualiserParam ruleComponentVisualiserParam)
    {
        return i18nHelper.getText("tutorial.if.condition.user.email.domain.name");
    }

    /**
     * Returns the label to use for this if condition rule component. The label appears below the name, and should
     * show at a glance the value of the configuration for this rule component. In our case, it will show the email
     * domain that has been configured by the user.
     *
     * If the email domain has not been configured, this will return {@code Option.none()}, which means no label is 
     * displayed.
     *
     */
    @Nonnull
    @Override
    public Option<String> getLabel(@Nonnull final RuleComponentVisualiserParam ruleComponentVisualiserParam)
    {
        final Option<String> configuredEmailDomainOpt =
                ruleComponentVisualiserParam.ruleConfiguration().getValue(UserEmailDomainIfCondition.EMAIL_DOMAIN_KEY);


        if(configuredEmailDomainOpt.isDefined())
        {
            // displays: is "domain.com"
            return some(i18nHelper.getText("tutorial.if.condition.user.email.domain.is") +
                    " \"" +
                    configuredEmailDomainOpt.get() + "\"");
        }
        else
        {
            return none(String.class);
        }
    }
}

This class makes use of I18nHelper to ensure our rule component is i18n ready. We just need to define the new property we reference in our  src/main/resources/servicedesk-automation-extension.properties  file by adding the following line:

tutorial.if.condition.user.email.domain.is=is

One last important step we can't forgot is to add a component import statement for the I18nHelper we use to GeneralOsgiImports, as this class is provided to us from another plugin:

@ComponentImport com.atlassian.jira.util.I18nHelper i18nHelper; 

5.4: Define the validator

The validate() method of our validator is invoked when a rule that our component is a part of is loaded or saved, and is responsible to determining if the configuration for our rule component is in a state our if condition can work with.

In our UserEmailDomainIfConditionValidator, we check that the email domain entered is both present and valid (for our simple tutorial, "valid" means "doesn't contain an @ symbol"). Here's the full implementation:

package com.atlassian.plugins.tutorial.servicedesk.ruleif;

import com.atlassian.fugue.Option;
import com.atlassian.jira.user.ApplicationUser;
import com.atlassian.jira.util.I18nHelper;
import com.atlassian.servicedesk.plugins.automation.api.configuration.ruleset.validation.ValidationResult;
import com.atlassian.servicedesk.plugins.automation.spi.ruleif.IfConditionValidator;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;

import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Maps.newHashMap;
import static org.apache.commons.lang3.StringUtils.isBlank;

/**
 * Responsible for checking that the email domain entered by the user is present and valid.
 *
 */
public final class UserEmailDomainIfConditionValidator implements IfConditionValidator
{
    private final static String EMAIL_DOMAIN_FIELD_NAME = "emailDomain";

    private final I18nHelper.BeanFactory i18nFactory;

    @Autowired
    public UserEmailDomainIfConditionValidator(final I18nHelper.BeanFactory i18nFactory)
    {
        this.i18nFactory = i18nFactory;
    }

    /**
     * This method is invoked whenever a rule that contains a user email domain if condition is saved, or loaded. If
     * a FAILED result is returned, the error message contained in the result will be displayed to the user, and
     * any save operation will be blocked.
     */
    @Override
    public ValidationResult validate(final IfConditionValidationParam ifConditionValidationParam)
    {
        final Option<String> configuredEmailDomainOpt =
                ifConditionValidationParam.getConfiguration().getData().getValue(UserEmailDomainIfCondition.EMAIL_DOMAIN_KEY);

        final ApplicationUser userToValidateWith = ifConditionValidationParam.getUserToValidateWith();

        if(configuredEmailDomainOpt.isEmpty() || isBlank(configuredEmailDomainOpt.get()))
        {
            return createResultWithFieldError(
                    userToValidateWith,
                    "tutorial.if.condition.user.email.domain.error.missing");
        }

        if(configuredEmailDomainOpt.get().contains("@"))
        {
            return createResultWithFieldError(
                    userToValidateWith,
                    "tutorial.if.condition.user.email.domain.error.invalid");
        }

        return ValidationResult.PASSED();
    }

    private ValidationResult createResultWithFieldError(@Nonnull ApplicationUser user, @Nonnull String errorI18nKey)
    {
        final I18nHelper i18nHelper = i18nFactory.getInstance(user);

        Map<String, List<String>> errorList = newHashMap();
        errorList.put(EMAIL_DOMAIN_FIELD_NAME, newArrayList(i18nHelper.getText(errorI18nKey)));

        return ValidationResult.FAILED(errorList);
    }
}

You'll notice in our validator that we defined two new error properties, so let's add these to  src/main/resources/servicedesk-automation-extension.properties:

tutorial.if.condition.user.email.domain.error.missing=Email domain is required
tutorial.if.condition.user.email.domain.error.invalid=Invalid email domain

5.5: Write the front-end resources

Earlier in our atlassian-plugin.xml, we defined our web-form-module as servicedesk/settings/automation/tutorial/modules/ruleif/useremaildomain-if-condition-form. Now we need to create the Soy template and Javascript files that make up this web form module.

First, let's create the location in which we'll place these files, and tell the plugin system where it can find them. Create the following directory under src/main/resources:

servicedesk/settings/automation/tutorial/modules

Now add the following to atlassian-plugin.xml:

<client-resource key="servicedesk-modules-automation-resources">
    <context>sd.project.admin</context>
    <directory location="servicedesk/settings/automation/tutorial/modules" />
</client-resource>

The context value above tells the automation plugin in which parts of Service Desk it should load these resources. In our case, we only want them to be used in project administration.

Ok, now that our front-end resources can be found, let's start creating them for our if condition. As we'll be creating resources for our then action in the following part of this guide, let's keep our if condition resources in their own folder. Create one under src/main/resources/servicedesk/settings/automation/tutorial/modules named "rule if".

In this directory, we're going to create four files:

File Purpose
useremaildomain-if-condition.js

This contains the front-end logic for:

  • Rendering our rule component UI
  • Client-side validation
  • Rendering errors
  • Serialising any input before being sent to the server

It is here that we will define the AMD module referenced by atlassian-plugin.xml.

useremaildomain-if-condition.soy This contains the template HTML that will be displayed inside the rule component box. This is where you define the actual HTML form inputs for your data, using Soy. For more information on writing Soy templates, see this guide.
useremaildomain-if-condition-model.js   This is where we define the Backbone.js model for our rule component data. In our case, the data will contain just a single user email domain string.
useremaildomain-if-condition-view.js   This is where we define any custom UI logic for our rule component. In our case, no custom UI logic is required, so our implementation is going to be pretty light.

Technology agnostic

 In our examples, we use Backbone.js and Soy, but you're not restricted to these technologies. The only restrictions placed on your front-end implementation are:

  • You must define an AMD module with the name referenced in your atlassian-plugin.xml.
  • You must implement the required methods of the Javascript SPI:

    render: function(config, errors)
    This is where you return the HTML of your front-end, as a Javascript string. We use Soy for this purpose in our examples, but you can use any client-side templating language you like.
    serialize: function ()
    This is where you convert the user input into a JSON format for transmission to the server-side automation API.

Most of the interesting stuff happens in  useremaildomain-if-condition.js , so let's look at that in more detail:

define("servicedesk/settings/automation/tutorial/modules/ruleif/useremaildomain-if-condition-form", [
    "servicedesk/jQuery",
    "servicedesk/underscore",
    "servicedesk/settings/automation/tutorial/modules/ruleif/useremaildomain-if-condition-model",
    "servicedesk/settings/automation/tutorial/modules/ruleif/useremaildomain-if-condition-view"
], function (
        $,
        _,
        UserEmailDomainModel,
        UserEmailDomainView
) {

    var userEmailDomainView = function(controller) {
        var template = ServiceDesk.Templates.Agent.Settings.Automation.Tutorial.Modules.RuleIf.serviceDeskUserEmailDomainIfConditionContainer;
        var $el = $(controller.el); // controller.el here is the parent container of the form.

		// Register event handlers
		// Listen to the 'destroy' event, which is fired when the form is disposed. This is used so we can clean up the resources.
        controller.on('destroy', onDestroy.bind(this));

		// Listen to the 'error' event, which is fired when the form validation fails.
        controller.on('error', onError.bind(this));
 
		// Render the errors onto the form. This is called when the 'error' event is fired.
        function onError(errors) {
            $el.find('.error').remove();
            _applyFieldErrors(errors.fieldErrors);
            _applyGlobalErrors(errors.globalErrors);
        }

		// Detach event handlers, this is called when the form is disposed.
        function onDestroy() {
            controller.off('destroy');
            controller.off('error');
        }

		// Functions to render the errors onto the form. 
		// The controller provides two helper methods for rendering, renderFieldError and renderGlobalError.
        function _applyFieldErrors(errors) {
            // If errors is an array
            _.each(errors, controller.renderFieldError)
        }

        function _applyGlobalErrors(errors) {
            for (var i = 0; i < errors.length; i++) {
                var thisError = errors[i];
                controller.renderGlobalError(thisError)
            }
        }
		

        return {
			/**
			 * The render method must be implemented, and is called to render the form onto the page. 
			 * config: A map of the current configuration of the component. This is identical in shape to what is generated by the serialize function.
			 * errors: An object the properties fieldErrors and globalErrors, which contain a list of errors.
			**/ 
            render: function(config, errors) {
                var emailDomain = config && config.emailDomain ? config.emailDomain : "";

                // Render the template
                $el.html(template());

                this.emailDomainView = new UserEmailDomainView({
                    model: new UserEmailDomainModel({
                        emailDomain: emailDomain
                    }),
                    el: $el.find(".automation-servicedesk-email-domain-if-condition-container")
                }).render();

				// Render the errors
                if (errors) {
                    if (errors.fieldErrors) {
                        _applyFieldErrors(errors.fieldErrors);
                    }

                    if (errors.globalErrors) {
                        _applyGlobalErrors(errors.globalErrors);
                    }
                }

                return this;
            },

			/**
			 * The serialize method must be implemented, and is called when the user tries to submit the form. This is called after validate.
			 * This method is expected to return a map containing the configuration of the form.
			**/ 
            serialize: function () {
                return {
                    emailDomain: $el.find('input').val()
                }
            },

			/**
			 * The validate method is optional, and is called after serialize.
			 * deferred: A jQuery deferred object. This must be resolved or rejected, if rejected the submit will fail. Note that rejecting
			 *           the deferred will not trigger an 'error' event.
			**/
            validate: function (deferred) {
                $el.find('.error').remove();
                var hasError = false;
                var emailDomainField = $el.find('input');
                var fieldErrors = {};

				// If the email domain field is empty, set a field error.
                if (!emailDomainField.val()) {
                    fieldErrors[emailDomainField.attr('name')] = AJS.I18n.getText('tutorial.if.condition.user.email.domain.error.missing');
                    hasError = true;
                }

                if (hasError) {
					// Render field error
                    _applyFieldErrors(fieldErrors);
 
					// Reject the deferred as there is an error and stop the submit action.
                    deferred.reject();
                }
                else {
					// Resolve the deferred and continue the submit action.
                    deferred.resolve();
                }
            },

			/**
			 * The dispose method is optional, and is called when the form is removed from the DOM.
			**/
            dispose: function() {
				// Clean up the email domain view resources.
                if (this.emailDomainView) {
                    this.emailDomainView.dispose && this.emailDomainView.dispose();
                }
            }
        }
    };

	// The AMD module is expected to return a function that takes the controller as a parameter.
    return function(controller) {
        return userEmailDomainView(controller);
    };
}); 

The first thing we do is include an AMD module definition, defining the servicedesk/settings/automation/tutorial/modules/ruleif/useremaildomain-if-condition-form  module we referenced in  atlassian-plugin.xml .

We then build up our SPI implementation, stored in the userEmailDomainView variable. This function constructs the HTML, making use of our Soy template to do this. It also makes use of the JS API methods provided by automation, namely on()of()renderFieldError() (for showing error messages for a specific field), and renderGlobalError() (for showing non-field specific errors).

Finally, this method implements the required SPI methods render() and serialise(), as well as implementing the two optional SPI methods:

validate: function (deferred)
This is where you include any client-side validation.
dispose: function()
This is where you stop listening to events etc.

Below you can find the full implementation of the other files making up our front-end resources. For brevity, I'm not going to go through the implementation of each in detail.

useremaildomain-if-condition.soy
{namespace ServiceDesk.Templates.Agent.Settings.Automation.Tutorial.Modules.RuleIf}

/**
 * Draw the container for the service desk user email domain if condition
 */
{template .serviceDeskUserEmailDomainIfConditionContainer}
    <div class="automation-servicedesk-email-domain-if-condition-container"></div>
{/template}

/**
 * Draw the contents of the user email domain form
 * @param emailDomain the user's email domain
 */
{template .drawUserEmailDomainForm}
    <div class="automation-servicedesk-email-domain-if-condition-header"><b>{getText('tutorial.if.condition.user.email.domain.prompt')}</b></div>
    <input type="text" name="emailDomain" class="textarea automation-servicedesk-comment-textarea mentionable" value="{$emailDomain}">
{/template}
 
useremaildomain-if-condition-model.js
 define("servicedesk/settings/automation/tutorial/modules/ruleif/useremaildomain-if-condition-model", [
    "servicedesk/backbone-brace"
], function (
        Brace
) {

    return Brace.Model.extend({
        namedAttributes: {
            emailDomain: String
        },
        defaults: {
            emailDomain: ""
        }
    });
});
useremaildomain-if-condition-view.js
 define("servicedesk/settings/automation/tutorial/modules/ruleif/useremaildomain-if-condition-view", [
    "servicedesk/jQuery",
    "servicedesk/underscore",
    "servicedesk/backbone-brace",
    "servicedesk/shared/mixin/form/form-mixin"
], function (
        $,
        _,
        Brace,
        FormMixin
) {
    return Brace.View.extend({
        template: ServiceDesk.Templates.Agent.Settings.Automation.Tutorial.Modules.RuleIf.drawUserEmailDomainForm,
        mixins: [FormMixin],

        dispose: function() {
            this.undelegateEvents();
            this.stopListening();
        },

        render: function() {
            this.$el.html(this.template(this.model.toJSON()));
            return this;
        }
    });
});

In our Soy template, you'll notice that we reference a new property string, tutorial.if.condition.user.email.domain.prompt. Let's define this in our src/main/resources/servicedesk-automation-extension.properties file:

tutorial.if.condition.user.email.domain.prompt=Email domain (e.g. "gmail.com")

One last thing we need to do is add a new class to GeneralOsgiImports. When we access property strings in our Soy templates, behind the scenes it makes use of I18nHelper.BeanFactory, so let's add that:

@ComponentImport com.atlassian.jira.util.I18nHelper.BeanFactory i18nBeanFactory;

5.6: Check that it works

Before we fire up JIRA and check that we can create a rule with our new if condition, let's do a quick spot check of our changes to the files we created as part of previous sections. Here's how the following files should now look:

atlassian-plugin.xml
<atlassian-plugin key="com.atlassian.plugins.tutorial.servicedesk.servicedesk-automation-extension" name="${project.name}" plugins-version="2">
    <plugin-info>
        <description>${project.description}</description>
        <version>${project.version}</version>
        <vendor name="${project.organization.name}" url="${project.organization.url}" />
        <param name="plugin-icon">images/pluginIcon.png</param>
        <param name="plugin-logo">images/pluginLogo.png</param>
    </plugin-info>

    <!-- add our i18n resource -->
    <resource type="i18n" name="i18n" location="i18n/servicedesk-automation-extension"/>

    <automation-rule-when-handler key="issue-assignee-changed-tutorial-when-handler" name="Issue assignee changed" name-i18n-key="tutorial.when.handler.issue.assignee.changed">
        <icon-class>bp-jira</icon-class>
        <provides>
            <provide>issue</provide>
            <provide>user</provide>
        </provides>
    </automation-rule-when-handler>

    <automation-rule-event-when-handler key="issue-assignee-changed-tutorial-event-when-handler"
                                        class="com.atlassian.plugins.tutorial.servicedesk.when.AssigneeChangedEventWhenHandler">
        <automation-rule-when-handler module-key="issue-assignee-changed-tutorial-when-handler" />
    </automation-rule-event-when-handler>

    <automation-rule-if-condition key="user-email-domain-tutorial-if-condition" class="com.atlassian.plugins.tutorial.servicedesk.ruleif.UserEmailDomainIfCondition" name="User email domain"
                                  name-i18n-key="tutorial.if.condition.user.email.domain.name">
        <icon-class>user</icon-class>
        <requires>
            <require>user</require>
        </requires>
        <visualiser class="com.atlassian.plugins.tutorial.servicedesk.ruleif.UserEmailDomainIfConditionVisualiser" />
        <validator class="com.atlassian.plugins.tutorial.servicedesk.ruleif.UserEmailDomainIfConditionValidator"/>
        <web-form-module>servicedesk/settings/automation/tutorial/modules/ruleif/useremaildomain-if-condition-form</web-form-module>
    </automation-rule-if-condition>

    <client-resource key="servicedesk-modules-automation-resources">
        <context>sd.project.admin</context>
        <directory location="servicedesk/settings/automation/tutorial/modules" />
    </client-resource>
    
</atlassian-plugin> 
GeneralOsgiImports.java
package com.atlassian.plugins.tutorial.servicedesk.osgi;

import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;

/**
 * This class is used to replace <component-import /> declarations in the atlassian-plugin.xml.
 * This class will be scanned by the atlassian spring scanner at compile time.
 * There is no situations where you ever need to create this class, it's here purely so that all the component-imports
 * are in the one place and not scattered throughout the code.
 */
@SuppressWarnings("UnusedDeclaration")
public class GeneralOsgiImports
{
    /******************************
    // Automation Engine
    ******************************/
    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.whenhandler.WhenHandlerProjectContextService whenHandlerProjectContextService;
    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.whenhandler.WhenHandlerRunInContextService whenHandlerRunInContextService;

    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.message.helper.IssueMessageHelper issueMessageHelper;
    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.message.helper.CommentMessageHelper commentMessageHelper;
    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.message.helper.UserMessageHelper userMessageHelper;
    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.message.RuleMessageBuilderService ruleMessageBuilderService;
    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.error.IfConditionErrorHelper ifConditionErrorHelper;

    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.command.RuleExecutionCommandBuilderService ruleExecutionCommandBuilderService;

    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.configuration.ruleset.input.BuilderService builderService;

    /******************************
    // JIRA
    ******************************/
    @ComponentImport com.atlassian.jira.issue.IssueManager issueManager;
    @ComponentImport com.atlassian.jira.security.PermissionManager permissionManager;
    @ComponentImport com.atlassian.jira.user.util.UserManager userManager;
    @ComponentImport com.atlassian.jira.util.I18nHelper i18nHelper;
    @ComponentImport com.atlassian.jira.util.I18nHelper.BeanFactory i18nBeanFactory;

    private GeneralOsgiImports()
    {
        throw new Error("This class should not be instantiated");
    }
}
 
servicedesk-automation-extension.properties
tutorial.when.handler.issue.assignee.changed=Issue assignee changed
tutorial.if.condition.user.email.domain.name=User email domain
tutorial.if.condition.user.email.domain.prompt=Email domain (e.g. "gmail.com")
tutorial.if.condition.user.email.domain.is=is
tutorial.if.condition.user.email.domain.error.missing=Email domain is required
tutorial.if.condition.user.email.domain.error.invalid=Invalid email domain 

Ok, now we're ready to test our changes out. 

Once again, follow steps 2-6 of the JIRA Service Desk plugin development flow. Once you've installed the plugin in your running JIRA instance, open the Settings tab on any Service Desk (create a Service Desk if one does not yet exist), and select 'Automation'.

Add an automation rule, using 'Custom rule' as the template. Select 'Issue assignee changed' for your WHEN, then click on your IF. You should now see a new 'User email domain' option when setting up the IF of a rule.

Ok, we can add it to a rule, but again lets see if it actually works. Create a complete rule, with 'User email domain' as the trigger. Pick an email address of a user you wish to test with:

Now, as previously, make our THEN a comment that makes it obvious the rule has worked:

Now give the rule a name and save it, then lets test it out. To actually check our if condition works, you'll need to have two users in your JIRA instance: one that has the domain you specified, and one that doesn't. Each of these users needs to have access to the project you created an automation rule for. You'll need to add the second user as an agent to the Service Desk via the 'People' tab.

Log in as a user that has an email address matching the domain you specified. Create or open an issue in same project you just set up the automation rule for. Now change the assignee. You should see a comment added automatically to the JIRA issue.

Now log in as a user that does not have a matching email address domain. Perform the same steps as above. This time, you should not see a comment added to the ticket. The if condition has prevented this rule from executing it's then action. Good job!

Step 6. Create your 'then' action

After all our hard work so far, it's now possible for us to tell if an issue has its assignee changed by a user from an external company (we are assuming that everyone uses their work email address). What do we want to do with this information? How about we add a label to our issue, so we can categorise the issues being managed externally. Let's do this by implementing an 'Add label' then action.

The steps to create our 'then' action are identical to creating an if condition: we define our module, define the then action implementation class, define our visualiser and validator, and write our front-end resources.

6.1: Define the module

Add the below to atlassian-plugin.xml:

<automation-rule-then-action key="issue-label-tutorial-then-action" class="com.atlassian.plugins.tutorial.servicedesk.rulethen.IssueLabelThenAction" name="Add label to issue"
                              name-i18n-key="tutorial.then.action.issue.label.name">
    <icon-class>bp-jira</icon-class>
    <requires>
        <require>issue</require>
    </requires>
    <visualiser class="com.atlassian.plugins.tutorial.servicedesk.rulethen.IssueLabelThenActionVisualiser" />
    <validator class="com.atlassian.plugins.tutorial.servicedesk.rulethen.IssueLabelThenActionValidator"/>
    <web-form-module>servicedesk/settings/automation/tutorial/modules/rulethen/issue-label-then-action-form</web-form-module>
</automation-rule-then-action> 

You've encountered all these properties in previous steps. As in those previous steps, we've defined a new property that defines or module name, so let's add that to src/main/resources/i18n/servicedesk-automation-extension.properties:

tutorial.then.action.issue.label.name=Add label to issue

6.2: Define the then action

This class does the actual work of adding a label to the JIRA issue, and must implement the ThenAction interface, which defines the following contract:

public interface ThenAction
{
    public Either<ThenActionError, RuleMessage> invoke(@Nonnull ThenActionParam thenActionParam);
    public static interface ThenActionParam
    {
        public ApplicationUser getUser();
        public ThenActionConfiguration getConfiguration();
        public RuleMessage getMessage();
    }
}

When a rule execution has reached it's then action component, invoke() is called with a ThenActionParam containing all the context needed by the then action implementation to do it's job. In our case, this parameter will contain the JIRA issue we wish to add a label to, as well as the configured label to add.

Here's our full implementation of IssueLabelThenAction:

package com.atlassian.plugins.tutorial.servicedesk.rulethen;

import com.atlassian.fugue.Either;
import com.atlassian.fugue.Option;
import com.atlassian.jira.bc.issue.label.LabelService;
import com.atlassian.jira.issue.Issue;
import com.atlassian.jira.user.ApplicationUser;
import com.atlassian.jira.util.ErrorCollection;
import com.atlassian.pocketknife.api.commons.error.AnError;
import com.atlassian.servicedesk.plugins.automation.api.execution.error.ThenActionError;
import com.atlassian.servicedesk.plugins.automation.api.execution.error.ThenActionErrorHelper;
import com.atlassian.servicedesk.plugins.automation.api.execution.message.RuleMessage;
import com.atlassian.servicedesk.plugins.automation.api.execution.message.helper.IssueMessageHelper;
import com.atlassian.servicedesk.plugins.automation.spi.rulethen.ThenAction;
import org.springframework.beans.factory.annotation.Autowired;

import javax.annotation.Nonnull;
import java.util.Iterator;

import static com.atlassian.fugue.Either.right;
import static com.atlassian.fugue.Option.some;

/**
 * Adds the label configured in its rule component to the JIRA issue which triggered the rule execution.
 *
 */
public final class IssueLabelThenAction implements ThenAction
{
    static final String ISSUE_LABEL_KEY = "issueLabel";

    private final IssueMessageHelper issueMessageHelper;
    private final ThenActionErrorHelper thenActionErrorHelper;
    private final LabelService labelService;

    @Autowired
    public IssueLabelThenAction(
            @Nonnull final IssueMessageHelper issueMessageHelper,
            @Nonnull final ThenActionErrorHelper thenActionErrorHelper,
            @Nonnull final LabelService labelService)
    {
        this.issueMessageHelper = issueMessageHelper;
        this.thenActionErrorHelper = thenActionErrorHelper;
        this.labelService = labelService;
    }

    /**
     * Retrieves the label to be added and the issue to add the label to from the supplied {@code thenActionParam}, and
     * adds this label to the issue.
     *
     * If there is any kind of issue or exception, returns a ThenActionError, otherwise returns the provided rule
     * message unmodified.
     */
    @Override
    public Either<ThenActionError, RuleMessage> invoke(final ThenActionParam thenActionParam)
    {
        // Get the label we want to add to the issue
        final Option<String> labelOpt = thenActionParam.getConfiguration().getData().getValue(ISSUE_LABEL_KEY);
        if(labelOpt.isEmpty())
        {
            return thenActionErrorHelper.error("No " + ISSUE_LABEL_KEY + " property in config data");
        }
        final String labelToAdd = labelOpt.get();

        // Get the issue to which we're adding the label
        final Either<AnError, Issue> issueEither = issueMessageHelper.getIssue(thenActionParam.getMessage());
        if (issueEither.isLeft())
        {
            // We don't perform any task if we can't get the issue from the rule message
            return thenActionErrorHelper.error(issueEither.left().get());
        }
        final Issue issueToAddLabelTo = issueEither.right().get();

        final ApplicationUser userAddingLabel = thenActionParam.getUser();
        try
        {
            Option<ErrorCollection> addLabelErrors = addLabelToIssue(labelToAdd, issueToAddLabelTo, userAddingLabel);
            if(addLabelErrors.isDefined())
            {
                return thenActionErrorHelper.error(
                        createErrorMessage(
                                labelToAdd,
                                issueToAddLabelTo,
                                toPrintable(addLabelErrors.get())
                        )
                );
            }
        }
        catch (Exception e)
        {
            return thenActionErrorHelper.error(
                    createErrorMessage(
                            labelToAdd,
                            issueToAddLabelTo,
                            e.getMessage()
                    )
            );
        }

        return right(thenActionParam.getMessage());
    }

    private Option<ErrorCollection> addLabelToIssue(final String labelToAdd, final Issue issueToAddLabelTo, final ApplicationUser userAddingLabel)
    {
        final LabelService.AddLabelValidationResult validationResult =
                labelService.validateAddLabel(userAddingLabel, issueToAddLabelTo.getId(), labelToAdd);
        if(!validationResult.isValid())
        {
            return some(validationResult.getErrorCollection());
        }

        labelService.addLabel(userAddingLabel, validationResult, false);
        return Option.none(ErrorCollection.class);
    }

    private String createErrorMessage(final String labelBeingAdded, final Issue issueToAddLabelTo, final String errorMessage)
    {
        return String.format(
                "Unable to add label '%s' to issue with key '%s' due to the following: %s",
                labelBeingAdded,
                issueToAddLabelTo != null ? issueToAddLabelTo.getKey() : "null",
                errorMessage);
    }

    private String toPrintable(final ErrorCollection errorCollection)
    {
        final StringBuilder errorMessage = new StringBuilder();
        for(final Iterator<String> errorIter = errorCollection.getErrorMessages().iterator(); errorIter.hasNext();)
        {
            errorMessage.append(errorIter.next()).append(";");
            if(errorIter.hasNext())
            {
                errorMessage.append(" ");
            }
        }

        return errorMessage.toString();
    }
}

You'll notice here that we made use of the JIRA-provided LabelService to add the label to the issue. Because this is provided outside of our plugin, we need to add the following to GeneralOsgiImports:

@ComponentImport com.atlassian.jira.bc.issue.label.LabelService labelService;

We also make use of ThenActionErrorHelper, which is provided by another plugin, so we must add the following to GeneralOsgiImports:

@ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.error.ThenActionErrorHelper thenActionErrorHelper;

6.3: Define the visualiser

In precisely the same way as in our if condition, the visualiser lets us control what's displayed in the rule UI when our then action component is configured. Naturally, we want to display the label that will be added to issues for our issue label then action. We implement the same interface as we did for our if condition, namely RuleComponentVisualiser. Here's the full implementation:

package com.atlassian.plugins.tutorial.servicedesk.rulethen;

import com.atlassian.fugue.Option;
import com.atlassian.jira.util.I18nHelper;
import com.atlassian.plugins.tutorial.servicedesk.ruleif.UserEmailDomainIfCondition;
import com.atlassian.servicedesk.plugins.automation.spi.visualiser.RuleComponentVisualiser;
import org.springframework.beans.factory.annotation.Autowired;

import javax.annotation.Nonnull;

import static com.atlassian.fugue.Option.none;
import static com.atlassian.fugue.Option.some;

/**
 * This visualiser is responsible for deciding what name and label to show for issue label then action rule components.
 * The name never changes, but the label displayed will show the label, if this has been configured.
 *
 */
public final class IssueLabelThenActionVisualiser implements RuleComponentVisualiser
{
    private final I18nHelper i18nHelper;

    @Autowired
    public IssueLabelThenActionVisualiser(final I18nHelper i18nHelper)
    {
        this.i18nHelper = i18nHelper;
    }

    /**
     * Returns the name to use for this then action rule component. The name appears above the label, adjacent to the
     * rule component icon.
     */
    @Nonnull
    @Override
    public String getName(final RuleComponentVisualiserParam ruleComponentVisualiserParam)
    {
        return i18nHelper.getText("tutorial.then.action.issue.label.name");
    }

    /**
     * Returns the label to use for this then action rule component. The label appears below the name, and should
     * show at a glance the value of the configuration for this rule component. In our case, it will show the label
     * that has been configured by the user.
     *
     * If the label has not been configured, this will return {@code Option.none()}, which means no label is
     * displayed.
     *
     */
    @Nonnull
    @Override
    public Option<String> getLabel(@Nonnull final RuleComponentVisualiserParam ruleComponentVisualiserParam)
    {
        final Option<String> configuredLabelOpt =
                ruleComponentVisualiserParam.ruleConfiguration().getValue(IssueLabelThenAction.ISSUE_LABEL_KEY);

        if(configuredLabelOpt.isDefined())
        {
            return some("\"" + configuredLabelOpt.get() + "\"");
        }
        else
        {
            return none(String.class);
        }
    }
}

6.4: Define the validator

Our validator is implemented in an identical way to our if condition validator, with one exception: the implementation class must implement the ThenActionValidator interface. Here's the full implementation:

package com.atlassian.plugins.tutorial.servicedesk.rulethen;

import com.atlassian.fugue.Option;
import com.atlassian.jira.user.ApplicationUser;
import com.atlassian.jira.util.I18nHelper;
import com.atlassian.plugins.tutorial.servicedesk.ruleif.UserEmailDomainIfCondition;
import com.atlassian.servicedesk.plugins.automation.api.configuration.ruleset.validation.ValidationResult;
import com.atlassian.servicedesk.plugins.automation.spi.rulethen.ThenActionValidator;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;

import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Maps.newHashMap;
import static org.apache.commons.lang3.StringUtils.isBlank;

/**
 * Responsible for checking that the label entered by the user is a valid label.
 *
 */
public final class IssueLabelThenActionValidator implements ThenActionValidator
{
    private final static String ISSUE_LABEL_FIELD_NAME = "issueLabel";

    private final I18nHelper.BeanFactory i18nFactory;

    @Autowired
    public IssueLabelThenActionValidator(final I18nHelper.BeanFactory i18nFactory)
    {
        this.i18nFactory = i18nFactory;
    }

    /**
     * This method is invoked whenever a rule that contains an issue label then action is saved, or loaded. If
     * a FAILED result is returned, the error message contained in the result will be displayed to the user, and
     * any save operation will be blocked.
     */
    @Override
    public ValidationResult validate(final ThenActionValidationParam thenActionValidationParam)
    {
        final Option<String> configuredLabel =
                thenActionValidationParam.getConfiguration().getData().getValue(IssueLabelThenAction.ISSUE_LABEL_KEY);

        final ApplicationUser userToValidateWith = thenActionValidationParam.getUserToValidateWith();

        // For tutorial purposes, we just check the label is not blank
        if(configuredLabel.isEmpty() || isBlank(configuredLabel.get()))
        {
            return createResultWithFieldError(
                    userToValidateWith,
                    "tutorial.then.action.issue.label.error.missing");
        }

        return ValidationResult.PASSED();
    }

    private ValidationResult createResultWithFieldError(@Nonnull ApplicationUser user, @Nonnull String errorI18nKey)
    {
        final I18nHelper i18nHelper = i18nFactory.getInstance(user);

        Map<String, List<String>> errorList = newHashMap();
        errorList.put(ISSUE_LABEL_FIELD_NAME, newArrayList(i18nHelper.getText(errorI18nKey)));

        return ValidationResult.FAILED(errorList);
    }
}
 

We're keeping the validation pretty simple for this tutorial - we just check that the user has typed something.

We've added a new string property to indicate the missing label value, so let's add that to src/main/resources/servicedesk-automation-extension.properties as usual:

tutorial.then.action.issue.label.error.missing=Label is required

6.5: Write the front-end resources

Again this is going to be a very similar process to how we created our if condition front-end resources. First, let's create a place for our then action front-end resources to reside. Create a "rulethen" directory under src/main/resources/servicedesk/settings/automation/tutorial/modules.

As with our if condition, in this directory we're going to create the following files:

  • issuelabel-then-action.js
  • issuelabel-then-action.soy
  • issuelabel-then-action-model.js
  • issuelabel-then-action-view.js

After defining our if condition front-end resources, there's nothing new here, so I'm going to keep it brief and provide the full implementation of these files, without going into too much detail:

issuelabel-then-action.js
 define("servicedesk/settings/automation/tutorial/modules/rulethen/issue-label-then-action-form", [
    "servicedesk/jQuery",
    "servicedesk/underscore",
    "servicedesk/settings/automation/tutorial/modules/rulethen/issue-label-then-action-model",
    "servicedesk/settings/automation/tutorial/modules/rulethen/issue-label-then-action-view"
], function (
        $,
        _,
        IssueLabelModel,
        IssueLabelView
) {

    var issueLabelView = function(controller) {
        var template = ServiceDesk.Templates.Agent.Settings.Automation.Tutorial.Modules.RuleThen.serviceDeskIssueLabelThenActionContainer;
        var $el = $(controller.el);

        function onError(errors) {
            $el.find('.error').remove();
            _applyFieldErrors(errors.fieldErrors);
            _applyGlobalErrors(errors.globalErrors);
        }

        function onDestroy() {
            controller.off('destroy');
            controller.off('error');
        }

        function _applyFieldErrors(errors) {
            // If errors is an array
            _.each(errors, controller.renderFieldError)
        }

        function _applyGlobalErrors(errors) {
            for (var i = 0; i < errors.length; i++) {
                var thisError = errors[i];
                controller.renderGlobalError(thisError)
            }
        }

        controller.on('destroy', onDestroy.bind(this));
        controller.on('error', onError.bind(this));

        return {
            render: function(config, errors) {
                var issueLabel = config && config.issueLabel ? config.issueLabel : "";

                // Render the template
                $el.html(template());

                this.issueLabelView = new IssueLabelView({
                    model: new IssueLabelModel({
                        issueLabel: issueLabel
                    }),
                    el: $el.find(".automation-servicedesk-issue-label-then-action-container")
                }).render();

                if (errors) {
                    if (errors.fieldErrors) {
                        _applyFieldErrors(errors.fieldErrors);
                    }

                    if (errors.globalErrors) {
                        _applyGlobalErrors(errors.globalErrors);
                    }
                }

                return this;
            },

            serialize: function () {
                return {
                    issueLabel: $el.find('input').val()
                }
            },

            validate: function (deferred) {
                $el.find('.error').remove();
                var hasError = false;
                var issueLabelField = $el.find('input');
                var fieldErrors = {};

                if (!issueLabelField.val()) {
                    fieldErrors[issueLabelField.attr('name')] = AJS.I18n.getText('tutorial.then.action.issue.label.error.missing');
                    hasError = true;
                }

                if (hasError) {
                    _applyFieldErrors(fieldErrors);
                    deferred.reject();
                }
                else {
                    deferred.resolve();
                }
            },

            dispose: function() {
                if (this.issueLabelView) {
                    this.issueLabelView.dispose && this.issueLabelView.dispose();
                }
            }
        }
    };

    return function(controller) {
        return issueLabelView(controller);
    };
});
issuelabel-then-action.soy
 {namespace ServiceDesk.Templates.Agent.Settings.Automation.Tutorial.Modules.RuleThen}

/**
 * Draw the container for the service desk issue label then action
 */
{template .serviceDeskIssueLabelThenActionContainer}
    <div class="automation-servicedesk-issue-label-then-action-container"></div>
{/template}

/**
 * Draw the contents of the issue label form
 * @param issueLabel the issue label
 */
{template .drawIssueLabelForm}
    <div class="automation-servicedesk-issue-label-then-action-header"><b>{getText('tutorial.then.action.issue.label.prompt')}</b></div>
    <input type="text" name="emailDomain" value="{$issueLabel}">
{/template}
issuelabel-then-action-model.js
 define("servicedesk/settings/automation/tutorial/modules/rulethen/issue-label-then-action-model", [
    "servicedesk/backbone-brace"
], function (
        Brace
) {

    return Brace.Model.extend({
        namedAttributes: {
            issueLabel: String
        },
        defaults: {
            issueLabel: ""
        }
    });
});
issuelabel-then-action-view.js
define("servicedesk/settings/automation/tutorial/modules/rulethen/issue-label-then-action-view", [
    "servicedesk/jQuery",
    "servicedesk/underscore",
    "servicedesk/backbone-brace",
    "servicedesk/shared/mixin/form/form-mixin"
], function (
        $,
        _,
        Brace,
        FormMixin
) {
    return Brace.View.extend({
        template: ServiceDesk.Templates.Agent.Settings.Automation.Tutorial.Modules.RuleThen.drawIssueLabelForm,
        mixins: [FormMixin],

        dispose: function() {
            this.undelegateEvents();
            this.stopListening();
        },

        render: function() {
            this.$el.html(this.template(this.model.toJSON()));
            return this;
        }
    });
});

Our Soy templates references tutorial.then.action.issue.label.prompt, so let's add this to src/main/resources/servicedesk-automation-extension.properties:

tutorial.then.action.issue.label.prompt=Label

6.6: Check that it works

Now we're ready to check that our issue label then action is working as expected. Before we do so, let's do a quick check of the changes we've made to existing files. Here's how they should now look:

atlassian-plugin.xml
<atlassian-plugin key="com.atlassian.plugins.tutorial.servicedesk.servicedesk-automation-extension" name="${project.name}" plugins-version="2">
    <plugin-info>
        <description>${project.description}</description>
        <version>${project.version}</version>
        <vendor name="${project.organization.name}" url="${project.organization.url}" />
        <param name="plugin-icon">images/pluginIcon.png</param>
        <param name="plugin-logo">images/pluginLogo.png</param>
    </plugin-info>

    <!-- add our i18n resource -->
    <resource type="i18n" name="i18n" location="i18n/servicedesk-automation-extension"/>

    <automation-rule-when-handler key="issue-assignee-changed-tutorial-when-handler" name="Issue assignee changed" name-i18n-key="tutorial.when.handler.issue.assignee.changed">
        <icon-class>bp-jira</icon-class>
        <provides>
            <provide>issue</provide>
            <provide>user</provide>
        </provides>
    </automation-rule-when-handler>

    <automation-rule-event-when-handler key="issue-assignee-changed-tutorial-event-when-handler"
                                        class="com.atlassian.plugins.tutorial.servicedesk.when.AssigneeChangedEventWhenHandler">
        <automation-rule-when-handler module-key="issue-assignee-changed-tutorial-when-handler" />
    </automation-rule-event-when-handler>

    <automation-rule-if-condition key="user-email-domain-tutorial-if-condition" class="com.atlassian.plugins.tutorial.servicedesk.ruleif.UserEmailDomainIfCondition" name="User email domain"
                                  name-i18n-key="tutorial.if.condition.user.email.domain.name">
        <icon-class>user</icon-class>
        <requires>
            <require>user</require>
        </requires>
        <visualiser class="com.atlassian.plugins.tutorial.servicedesk.ruleif.UserEmailDomainIfConditionVisualiser" />
        <validator class="com.atlassian.plugins.tutorial.servicedesk.ruleif.UserEmailDomainIfConditionValidator"/>
        <web-form-module>servicedesk/settings/automation/tutorial/modules/ruleif/useremaildomain-if-condition-form</web-form-module>
    </automation-rule-if-condition>

    <automation-rule-then-action key="issue-label-tutorial-then-action" class="com.atlassian.plugins.tutorial.servicedesk.rulethen.IssueLabelThenAction" name="Add label to issue"
                                  name-i18n-key="tutorial.then.action.issue.label.name">
        <icon-class>bp-jira</icon-class>
        <requires>
            <require>issue</require>
        </requires>
        <visualiser class="com.atlassian.plugins.tutorial.servicedesk.rulethen.IssueLabelThenActionVisualiser" />
        <validator class="com.atlassian.plugins.tutorial.servicedesk.rulethen.IssueLabelThenActionValidator"/>
        <web-form-module>servicedesk/settings/automation/tutorial/modules/rulethen/issue-label-then-action-form</web-form-module>
    </automation-rule-then-action>

    <client-resource key="servicedesk-modules-automation-resources">
        <context>sd.project.admin</context>
        <directory location="servicedesk/settings/automation/tutorial/modules" />
    </client-resource>
    
</atlassian-plugin>
GeneralOsgiImports.java
package com.atlassian.plugins.tutorial.servicedesk.osgi;

import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;

/**
 * This class is used to replace <component-import /> declarations in the atlassian-plugin.xml.
 * This class will be scanned by the atlassian spring scanner at compile time.
 * There is no situations where you ever need to create this class, it's here purely so that all the component-imports
 * are in the one place and not scattered throughout the code.
 */
@SuppressWarnings("UnusedDeclaration")
public class GeneralOsgiImports
{
    /******************************
    // Automation Engine
    ******************************/
    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.whenhandler.WhenHandlerProjectContextService whenHandlerProjectContextService;
    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.whenhandler.WhenHandlerRunInContextService whenHandlerRunInContextService;

    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.message.helper.IssueMessageHelper issueMessageHelper;
    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.message.helper.CommentMessageHelper commentMessageHelper;
    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.message.helper.UserMessageHelper userMessageHelper;
    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.message.RuleMessageBuilderService ruleMessageBuilderService;
    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.error.IfConditionErrorHelper ifConditionErrorHelper;
    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.error.ThenActionErrorHelper thenActionErrorHelper;

    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.execution.command.RuleExecutionCommandBuilderService ruleExecutionCommandBuilderService;

    @ComponentImport com.atlassian.servicedesk.plugins.automation.api.configuration.ruleset.input.BuilderService builderService;

    /******************************
    // JIRA
    ******************************/
    @ComponentImport com.atlassian.jira.issue.IssueManager issueManager;
    @ComponentImport com.atlassian.jira.security.PermissionManager permissionManager;
    @ComponentImport com.atlassian.jira.user.util.UserManager userManager;
    @ComponentImport com.atlassian.jira.util.I18nHelper i18nHelper;
    @ComponentImport com.atlassian.jira.util.I18nHelper.BeanFactory i18nBeanFactory;
    @ComponentImport com.atlassian.jira.bc.issue.label.LabelService labelService;

    private GeneralOsgiImports()
    {
        throw new Error("This class should not be instantiated");
    }
}
servicedesk-automation-extension.properties
tutorial.when.handler.issue.assignee.changed=Issue assignee changed
tutorial.if.condition.user.email.domain.name=User email domain
tutorial.if.condition.user.email.domain.prompt=Email domain (e.g. "gmail.com")
tutorial.if.condition.user.email.domain.is=is
tutorial.if.condition.user.email.domain.error.missing=Email domain is required
tutorial.if.condition.user.email.domain.error.invalid=Invalid email domain
tutorial.then.action.issue.label.name=Add label to issue
tutorial.then.action.issue.label.error.missing=Label is required
tutorial.then.action.issue.label.prompt=Label

Now let's try and add labels to our issues via our new then action rule component.

For the third time, follow steps 2-6 of the JIRA Service Desk plugin development flow. Once you've installed the plugin in your running JIRA instance, open the Settings tab on any Service Desk (create a Service Desk if one does not yet exist), and select 'Automation'.

Add an automation rule, using 'Custom rule' as the template. Select 'Issue assignee changed' for your WHEN, and a specific email domain for your IF. You should now see a new 'Add label to issue' option when setting up the THEN of a rule.

Adding it to a rule is all well and good, but if it doesn't do what it promises, it's not any use. Create a complete rule, using all the components we've created:

  • 'Issue assignee changed' for the WHEN
  • 'User email domain' is "<your test domain>" for the IF
  • 'Add label' "external" to issue for the WHEN:

Now give the rule a name and save it. As before, you'll need to have two users in your JIRA instance: one that has the email domain you specified in the if condition, and one that doesn't. Each of these users needs to have access to the project you created an automation rule for. You'll need to add the second user as an agent to the Service Desk via the 'People' tab.

Drumroll please

Log in as a user that has an email address matching the domain you specified. Create or open an issue in same project you just set up the automation rule for. Now change the assignee. You should see a label added to the ticket like so:

Now log in as a user that does not have a matching email address domain. Perform the same steps as above, except with a new issue. This time, you should not see a label added to the ticket. Woo! All of our rule components are working beautifully together, and we now have a super useful automation rule for adding labels when an external company changes the assignee of an issue:

Not only that, but each individual rule component you created can be used to make up other rules. Awesome, eh?

Go and have a cup of tea. You've earned it.

Was this page helpful?

Have a question about this article?

See questions about this article

Powered by Confluence and Scroll Viewport