Applicable: | This tutorial applies to Jira Service Management. 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. |
This tutorial shows you how to extend automation in Jira Service Management 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.
To get the most out of this tutorial, you should be familiar with:
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:
1 2git 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.
In a nutshell, automation in Jira Service Management 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.
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 Management |
---|---|---|
WHEN | Allows you to specify when an Automation Rule should be kicked off (e.g. what's triggering its execution) | By default, Jira Service Management will ship several WHEN triggers for you to use, including:
|
IF | Allows you to specify conditions to filter an Automation Rule execution depending if the condition is met or not. | By default, Jira Service Management will ship several IF conditions for you to use, depending on your selected WHEN trigger, including:
|
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 Management will ship several THEN actions for you to use, including:
|
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 8.0.0 and Service Management 4.0.0
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.
If you have not already set up the Atlassian Plugin SDK, do that now: Set up the Atlassian Plugin SDK and Build a Project.
In the directory where you want to put the plugin project, enter the following SDK command:
1 2atlas-create-jira-plugin
As prompted, enter the following information to identify your plugin:
group-id |
|
artifact-id |
|
version |
|
package |
|
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.
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:
Change to the project directory created by the SDK and open the pom.xml
file for editing.
Add your company or organisation name and your website as the name
and url
values of the organization
element:
1 2<organization> <name>Example Company</name> <url>http://www.example.com/</url> </organization>
Update the description
element:
1 2<description>Adds a new when, if and then rule component to Jira Service Management's automation feature.</description>
Save the file.
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:
1 2<atlassian-plugin key="${atlassian.plugin.key}" 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>
Atlassian Spring Scanner allows us to wire in OSGi dependencies via annotations, in a more convenient way than the old XML based configuration.
Open the root pom.xml and remove the dependency on atlassian-spring-scanner-runtime
.
Add a dependency on the Atlassian Spring scanner with provided scope. This will look like the following.
1 2<dependency> <groupId>com.atlassian.plugin</groupId> <artifactId>atlassian-spring-scanner-annotation</artifactId> <version>${atlassian.spring.scanner.version}</version> <scope>provided</scope> </dependency>
Don't forget to add in the version property:
1 2<atlassian.spring.scanner.version>2.0.1</atlassian.spring.scanner.version>
Add the Atlassian Spring Scanner Maven plugin to <build><plugins>
:
1 2<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>
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:
1 2<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.
We're going to create a 'when' handler that listens for when the assignee of a Service Management request changes. This will allow Service Management users to create an automation rule that does something when an assignee is updated.
Add the following to the atlassian-plugin.xml
file you modified earlier:
1 2<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 |
---|---|
| 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. |
| The non-internationalised name of your when handler. The name is what's displayed to end users in the UI. If there is no |
| 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 . |
| 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 . |
| 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:
Subsequently, when defining ifs and thens, you can specify which of these items of information you require, using
In our tutorial example, we're saying that our when handler will provide a |
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:
1 2tutorial.when.handler.issue.assignee.changed=Issue assignee changed
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:
1 2public 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 IssueEvent
s, 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:
1 2<dependency> <groupId>com.atlassian.servicedesk</groupId> <artifactId>jira-servicedesk-automation-api</artifactId> <version>${servicedesk.automation.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.atlassian.servicedesk</groupId> <artifactId>jira-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>
Don't forget to to add the appropriate version numbers to <properties>
:
1 2<servicedesk.automation.version>4.0.0</servicedesk.automation.version> <springframework.version>5.1.6.RELEASE</springframework.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:
1 2package 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.api.automation.execution.whenhandler.WhenHandlerProjectContextService whenHandlerProjectContextService; @ComponentImport com.atlassian.servicedesk.api.automation.execution.whenhandler.WhenHandlerRunInContextService whenHandlerRunInContextService; @ComponentImport com.atlassian.servicedesk.api.automation.execution.message.helper.IssueMessageHelper issueMessageHelper; @ComponentImport com.atlassian.servicedesk.api.automation.execution.message.helper.CommentMessageHelper commentMessageHelper; @ComponentImport com.atlassian.servicedesk.api.automation.execution.message.helper.UserMessageHelper userMessageHelper; @ComponentImport com.atlassian.servicedesk.api.automation.execution.message.RuleMessageBuilderService ruleMessageBuilderService; @ComponentImport com.atlassian.servicedesk.api.automation.execution.command.RuleExecutionCommandBuilderService ruleExecutionCommandBuilderService; @ComponentImport com.atlassian.servicedesk.api.automation.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 jira-maven-plugin
configuration in the root pom.xml:
1 2<instructions> <Atlassian-Plugin-Key>${atlassian.plugin.key}</Atlassian-Plugin-Key> <!-- Add packages to export here --> <Export-Package> com.atlassian.plugins.tutorial.servicedesk.api, </Export-Package> <!-- Add packages import here --> <Import-Package> com.atlassian.servicedesk.api.automation.*, com.atlassian.servicedesk.spi.automation.*, * </Import-Package> <!-- Ensure plugin is spring powered --> <Spring-Context>*</Spring-Context> </instructions>
The full <plugin>
definition should look like this:
1 2<plugin> <groupId>com.atlassian.maven.plugins</groupId> <artifactId>jira-maven-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>${atlassian.plugin.key}</Atlassian-Plugin-Key> <!-- Add packages to export here --> <Export-Package> com.atlassian.plugins.tutorial.servicedesk.api, </Export-Package> <!-- Add packages import here --> <Import-Package> com.atlassian.servicedesk.api.automation.*, com.atlassian.servicedesk.spi.automation.*, * </Import-Package> <!-- Ensure plugin is spring powered --> <Spring-Context>*</Spring-Context> </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
:
1 2package 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.api.automation.execution.command.RuleExecutionCommand; import com.atlassian.servicedesk.api.automation.execution.command.RuleExecutionCommandBuilder; import com.atlassian.servicedesk.api.automation.execution.command.RuleExecutionCommandBuilderService; import com.atlassian.servicedesk.api.automation.execution.message.RuleMessage; import com.atlassian.servicedesk.api.automation.execution.message.RuleMessageBuilder; import com.atlassian.servicedesk.api.automation.execution.message.RuleMessageBuilderService; import com.atlassian.servicedesk.api.automation.execution.message.helper.IssueMessageHelper; import com.atlassian.servicedesk.api.automation.execution.whenhandler.WhenHandlerContext; import com.atlassian.servicedesk.spi.automation.rulewhen.event.EventWhenHandler; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.annotation.Nonnull; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; /** * Fires off rules when an issue is created */ public final class AssigneeChangedEventWhenHandler implements EventWhenHandler<IssueEvent> { @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<>(); 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.putValue("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:
issue
and user
, so we *must *add those to the rule message. requestSynchronousExecution(boolean)
method of RuleExecutionCommandBuilder
. In AssigneeChangedEventWhenHandler
, we asked that each rule execution happen asynchronously:1 2final 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:
1 2package com.atlassian.plugins.tutorial.servicedesk.when; 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.servicedesk.api.automation.execution.context.project.ProjectContext; import com.atlassian.servicedesk.api.automation.execution.whenhandler.WhenHandlerContext; import com.atlassian.servicedesk.api.automation.execution.whenhandler.WhenHandlerProjectContextService; import com.atlassian.servicedesk.api.automation.execution.whenhandler.WhenHandlerRunInContextService; import java.util.List; import javax.annotation.Nonnull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class ProjectAndUserChecker { @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) { try { final ProjectContext applicationProjectContext = whenHandlerProjectContextService.getApplicationProjectContext(context); final List<Project> projects = applicationProjectContext.getProjects(); if (projects.isEmpty()) { return true; } else { return projects.contains(project); } } catch (Exception ex) { return false; } } /** * Can the passed user browse the given issue? */ public boolean canBrowseIssue(@Nonnull WhenHandlerContext context, @Nonnull final Issue issue) { return whenHandlerRunInContextService.executeInContext(context, 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
:
1 2<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:
1 2<atlassian-plugin key="${atlassian.plugin.key}" 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>
Follow steps 2-6 of the Jira Service Management plugin development flow. Once you've installed the plugin in your running Jira instance, open the Settings tab on any Service Management (create a Service Management 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 Management 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!
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.
Add the following to the atlassian-plugin.xml
file:
1 2<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 key
, name
, name-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 |
---|---|
| This is analogous to See "Define the module" in the when handler component section for the other things we could add here. |
| 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:
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. |
| 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
Defining a validator for your rule component is optional. If you don't define one, the component will always pass validation. |
| 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:
1 2tutorial.if.condition.user.email.domain.name=User email domain
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
:
1 2package com.atlassian.plugins.tutorial.servicedesk.ruleif; import com.atlassian.jira.user.ApplicationUser; import com.atlassian.servicedesk.api.ExceptionMessage; import com.atlassian.servicedesk.api.automation.IfConditionException; import com.atlassian.servicedesk.api.automation.execution.message.helper.UserMessageHelper; import com.atlassian.servicedesk.spi.automation.ruleif.IfCondition; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; /** * If condition that checks whether a user's email address belongs to a specified domain. * */ public final class UserEmailDomainIfCondition implements IfCondition { static final String EMAIL_DOMAIN_KEY = "emailDomain"; private final UserMessageHelper userMessageHelper; @Autowired public UserEmailDomainIfCondition( final UserMessageHelper userMessageHelper) { this.userMessageHelper = userMessageHelper; } /** * 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 {@literal true}, then rule execution halts and any * further 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 whether the condition has been met or not */ @Override public boolean matches(final IfConditionParam ifConditionParam) { // Get the email domain we want to check for final Optional<String> emailDomainOpt = ifConditionParam.getConfiguration().getData().getValue(EMAIL_DOMAIN_KEY); if (!emailDomainOpt.isPresent()) { throw new IfConditionException(new ExceptionMessage("sd.automation.email.config.missing", "No " + EMAIL_DOMAIN_KEY + " property in config data")); } final String emailDomainToCheckFor = emailDomainOpt.get(); // Get the email domain of the user that initiated the rule ApplicationUser userToCheck = userMessageHelper.getUser(ifConditionParam.getMessage(), UserMessageHelper.CURRENT_USER_USER_PREFIX); final String userEmailDomain = getEmailDomain(userToCheck); // Return the match result return 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 "emailDomain
" 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.
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:
1 2package com.atlassian.plugins.tutorial.servicedesk.ruleif; import com.atlassian.jira.util.I18nHelper; import com.atlassian.servicedesk.spi.automation.visualiser.RuleComponentVisualiser; import java.util.Optional; import javax.annotation.Nonnull; import org.springframework.beans.factory.annotation.Autowired; /** * 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 Optional.empty()}, which means no label is * displayed. * */ @Nonnull @Override public Optional<String> getLabel(@Nonnull final RuleComponentVisualiserParam ruleComponentVisualiserParam) { final Optional<String> configuredEmailDomainOpt = ruleComponentVisualiserParam.ruleConfiguration().getValue(UserEmailDomainIfCondition.EMAIL_DOMAIN_KEY); return configuredEmailDomainOpt.map(emailDomain -> i18nHelper.getText("tutorial.if.condition.user.email.domain.is") + " \"" + emailDomain +"\""); } }
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:
1 2tutorial.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:
1 2@ComponentImport com.atlassian.jira.util.I18nHelper i18nHelper;
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:
1 2package com.atlassian.plugins.tutorial.servicedesk.ruleif; import com.atlassian.jira.user.ApplicationUser; import com.atlassian.jira.util.I18nHelper; import com.atlassian.servicedesk.api.automation.configuration.ruleset.validation.ValidationResult; import com.atlassian.servicedesk.spi.automation.ruleif.IfConditionValidator; import java.util.List; import java.util.Map; import java.util.Optional; import javax.annotation.Nonnull; import org.springframework.beans.factory.annotation.Autowired; 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 Optional<String> configuredEmailDomainOpt = ifConditionValidationParam.getConfiguration().getData().getValue(UserEmailDomainIfCondition.EMAIL_DOMAIN_KEY); final ApplicationUser userToValidateWith = ifConditionValidationParam.getUserToValidateWith(); if (!configuredEmailDomainOpt.isPresent() || 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
:
1 2tutorial.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
:
1 2servicedesk/settings/automation/tutorial/modules
Now add the following to atlassian-plugin.xml
:
1 2<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 Management 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 "ruleif
".
In this directory, we're going to create four files:
File | Purpose |
---|---|
useremaildomain-if-condition.js | This contains the front-end logic for:
It is here that we will define the AMD module referenced by |
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:
1 2define("servicedesk/settings/automation/tutorial/modules/ruleif/useremaildomain-if-condition-form", [ "jquery", "automation/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:
| This is where you include any client-side validation. |
| 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
1 2{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
1 2define("servicedesk/settings/automation/tutorial/modules/ruleif/useremaildomain-if-condition-model", [ "automation/backbone-brace" ], function ( Brace ) { return Brace.Model.extend({ namedAttributes: { emailDomain: String }, defaults: { emailDomain: "" } }); });
useremaildomain-if-condition-view.js
1 2define("servicedesk/settings/automation/tutorial/modules/ruleif/useremaildomain-if-condition-view", [ "jquery", "automation/underscore", "automation/backbone-brace", "servicedesk/internal/agent/settings/automation/util/form-mixin/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:
1 2tutorial.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:
1 2@ComponentImport com.atlassian.jira.util.I18nHelper.BeanFactory i18nBeanFactory;
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
1 2<atlassian-plugin key="${atlassian.plugin.key}" 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"/> <client-resource key="servicedesk-modules-automation-resources"> <context>sd.project.admin</context> <directory location="servicedesk/settings/automation/tutorial/modules" /> </client-resource> <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> </atlassian-plugin>
GeneralOsgiImports.java
1 2package 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.api.automation.execution.whenhandler.WhenHandlerProjectContextService whenHandlerProjectContextService; @ComponentImport com.atlassian.servicedesk.api.automation.execution.whenhandler.WhenHandlerRunInContextService whenHandlerRunInContextService; @ComponentImport com.atlassian.servicedesk.api.automation.execution.message.helper.IssueMessageHelper issueMessageHelper; @ComponentImport com.atlassian.servicedesk.api.automation.execution.message.helper.CommentMessageHelper commentMessageHelper; @ComponentImport com.atlassian.servicedesk.api.automation.execution.message.helper.UserMessageHelper userMessageHelper; @ComponentImport com.atlassian.servicedesk.api.automation.execution.message.RuleMessageBuilderService ruleMessageBuilderService; @ComponentImport com.atlassian.servicedesk.api.automation.execution.command.RuleExecutionCommandBuilderService ruleExecutionCommandBuilderService; @ComponentImport com.atlassian.servicedesk.api.automation.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
1 2tutorial.when.handler.issue.assignee.changed=Issue assignee changed tutorial.if.condition.user.email.domain.name=User email domain 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.if.condition.user.email.domain.prompt=Email domain (e.g. "gmail.com")
Ok, now we're ready to test our changes out.
Once again, follow steps 2-6 of the Jira Service Management plugin development flow. Once you've installed the plugin in your running Jira instance, open the Settings tab on any Service Management (create a Service Management 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 Management 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!
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.
Add the below to atlassian-plugin.xml
:
1 2<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
:
1 2tutorial.then.action.issue.label.name=Add label to issue
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:
1 2public interface ThenAction { public 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
:
1 2package com.atlassian.plugins.tutorial.servicedesk.rulethen; import com.atlassian.jira.bc.issue.label.LabelService; import com.atlassian.jira.issue.Issue; import com.atlassian.jira.user.ApplicationUser; import com.atlassian.servicedesk.api.ExceptionMessage; import com.atlassian.servicedesk.api.automation.ThenActionException; import com.atlassian.servicedesk.api.automation.execution.message.RuleMessage; import com.atlassian.servicedesk.api.automation.execution.message.helper.IssueMessageHelper; import com.atlassian.servicedesk.spi.automation.rulethen.ThenAction; import java.util.Optional; import javax.annotation.Nonnull; import org.springframework.beans.factory.annotation.Autowired; /** * 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 LabelService labelService; @Autowired public IssueLabelThenAction( @Nonnull final IssueMessageHelper issueMessageHelper, @Nonnull final LabelService labelService) { this.issueMessageHelper = issueMessageHelper; 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 RuleMessage invoke(final ThenActionParam thenActionParam) { // Get the label we want to add to the issue final Optional<String> labelOpt = thenActionParam.getConfiguration().getData().getValue(ISSUE_LABEL_KEY); if (!labelOpt.isPresent()) { throw new ThenActionException(new ExceptionMessage("sd.automation.label.config.missing", "No " + ISSUE_LABEL_KEY + " property in config data")); } final String labelToAdd = labelOpt.get(); final Issue issueToAddLabelTo = issueMessageHelper.getIssue(thenActionParam.getMessage()); final ApplicationUser userAddingLabel = thenActionParam.getUser(); addLabelToIssue(labelToAdd, issueToAddLabelTo, userAddingLabel); return thenActionParam.getMessage(); } private void addLabelToIssue(final String labelToAdd, final Issue issueToAddLabelTo, final ApplicationUser userAddingLabel) { final LabelService.AddLabelValidationResult validationResult = labelService.validateAddLabel(userAddingLabel, issueToAddLabelTo.getId(), labelToAdd); if (!validationResult.isValid()) { throw new ThenActionException(new ExceptionMessage("sd.automation.error.on.label.addition", "Label " + labelToAdd + " couldn't be added to issue")); } labelService.addLabel(userAddingLabel, validationResult, false); } }
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
:
1 2@ComponentImport com.atlassian.jira.bc.issue.label.LabelService labelService;
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:
1 2package com.atlassian.plugins.tutorial.servicedesk.rulethen; import com.atlassian.jira.util.I18nHelper; import com.atlassian.servicedesk.spi.automation.visualiser.RuleComponentVisualiser; import java.util.Optional; import javax.annotation.Nonnull; import org.springframework.beans.factory.annotation.Autowired; /** * 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 Optional.none()}, which means no label is * displayed. * */ @Nonnull @Override public Optional<String> getLabel(@Nonnull final RuleComponentVisualiserParam ruleComponentVisualiserParam) { final Optional<String> configuredLabelOpt = ruleComponentVisualiserParam.ruleConfiguration().getValue(IssueLabelThenAction.ISSUE_LABEL_KEY); return configuredLabelOpt.map(label -> "\"" + label +"\""); } }
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:
1 2package com.atlassian.plugins.tutorial.servicedesk.rulethen; import com.atlassian.jira.user.ApplicationUser; import com.atlassian.jira.util.I18nHelper; import com.atlassian.servicedesk.api.automation.configuration.ruleset.validation.ValidationResult; import com.atlassian.servicedesk.spi.automation.rulethen.ThenActionValidator; import java.util.List; import java.util.Map; import java.util.Optional; import javax.annotation.Nonnull; import org.springframework.beans.factory.annotation.Autowired; 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 Optional<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.isPresent() || 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:
1 2tutorial.then.action.issue.label.error.missing=Label is required
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
1 2define("servicedesk/settings/automation/tutorial/modules/rulethen/issue-label-then-action-form", [ "jquery", "automation/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
1 2{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
1 2define("servicedesk/settings/automation/tutorial/modules/rulethen/issue-label-then-action-model", [ "automation/backbone-brace" ], function ( Brace ) { return Brace.Model.extend({ namedAttributes: { issueLabel: String }, defaults: { issueLabel: "" } }); });
issuelabel-then-action-view.js
1 2define("servicedesk/settings/automation/tutorial/modules/rulethen/issue-label-then-action-view", [ "jquery", "automation/underscore", "automation/backbone-brace", "servicedesk/internal/agent/settings/automation/util/form-mixin/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
:
1 2tutorial.then.action.issue.label.prompt=Label
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
1 2<atlassian-plugin key="${atlassian.plugin.key}" 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"/> <client-resource key="servicedesk-modules-automation-resources"> <context>sd.project.admin</context> <directory location="servicedesk/settings/automation/tutorial/modules" /> </client-resource> <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> </atlassian-plugin>
GeneralOsgiImports.java
1 2package 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.api.automation.execution.whenhandler.WhenHandlerProjectContextService whenHandlerProjectContextService; @ComponentImport com.atlassian.servicedesk.api.automation.execution.whenhandler.WhenHandlerRunInContextService whenHandlerRunInContextService; @ComponentImport com.atlassian.servicedesk.api.automation.execution.message.helper.IssueMessageHelper issueMessageHelper; @ComponentImport com.atlassian.servicedesk.api.automation.execution.message.helper.CommentMessageHelper commentMessageHelper; @ComponentImport com.atlassian.servicedesk.api.automation.execution.message.helper.UserMessageHelper userMessageHelper; @ComponentImport com.atlassian.servicedesk.api.automation.execution.message.RuleMessageBuilderService ruleMessageBuilderService; @ComponentImport com.atlassian.servicedesk.api.automation.execution.command.RuleExecutionCommandBuilderService ruleExecutionCommandBuilderService; @ComponentImport com.atlassian.servicedesk.api.automation.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
1 2tutorial.when.handler.issue.assignee.changed=Issue assignee changed tutorial.if.condition.user.email.domain.name=User email domain 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.if.condition.user.email.domain.prompt=Email domain (e.g. "gmail.com") 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 Management plugin development flow. Once you've installed the plugin in your running Jira instance, open the Settings tab on any Service Management (create a Service Management 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:
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 Management 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 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.
Rate this page: