Создание компонентов правила автоматизации

Применимость

Это руководство относится к JIRA Service Desk Server 3.2 и более поздним версиям.

 

Для информации, связанной с облаком, см. Выполнение действий по автоматизации.

Уровень опыта

Это расширенный учебник. Вы должны были пройти хотя бы один промежуточный учебник, прежде чем работать с этим учебником. Просмотрите этот учебник или тот, или просмотрите полный список учебников в DAC.

Оценка времени

Для завершения этого урока вам потребуется около 1 часа.

 

Обзор учебника

В этом руководстве показано, как расширить автоматизацию в JIRA Service Desk с помощью новых компонентов правил, которые вы можете создать. Вы узнаете, как построить каждый тип компонента правила («когда», «если» и «затем»). После того, как вы построите свои новые компоненты, они будут доступны для использования с помощью построителя правил автоматизации.

РИСУНОК

Необходимые знания

Чтобы получить максимальную отдачу от этого урока, вы должны быть знакомы с:

  • Основы разработки Java, такие как классы, интерфейсы, методы и т. д.
  • Как использовать и администрировать JIRA.

Источник плагина

Мы рекомендуем вам проработать этот учебник. Если вы хотите пропустить или проверить свою работу, когда закончите, вы можете найти исходный код плагина на Atlassian Bitbucket. Bitbucket служит публичному репозиторию Git, содержащему код учебника. Чтобы клонировать репозиторий, выполните следующую команду:


git clone https://bitbucket.org/atlassian/service-desk-automation-tutorial.git

Кроме того, вы можете загрузить исходный код в виде ZIP-архива со страницы загрузки.

Но прежде чем мы начнем, давайте немного поговорим о возможности, которую мы расширяем.

Что такое автоматизация?

В двух словах автоматизация в JIRA Service Desk позволяет пользователям службы поддержки автоматизировать повторяющиеся задачи, а также позволяет им избегать пропуска важных событий. Чтобы узнать, как это работает, ознакомьтесь с руководством пользователя.

Компоненты правил и правила

Автоматизация состоит из правил автоматизации, которые выполняют действия (например, агент оповещений), инициируемые при возникновении определенных событий (например, проблема), только если выполнены условия (например, проблема с высоким приоритетом).Эти 3 понятия называются «Компоненты правил автоматизации» и образуют полностью определенное «правило автоматизации»:

Тип компонента правила

Детали

Доступно в службе поддержки JIRA

 

 

 

 

 

 

 

 

 

WHEN

 

 

 

 

 

 

 

 

 

Позволяет указать, когда должно быть отменено правило автоматизации (например, что запускает его выполнение)

По умолчанию JIRA Service Desk отправит несколько триггеров WHEN для вас, включая:

  • Комментарий добавлен в задачу
  • Комментарий отредактирован в задаче
  • Создается задача
  • Разрешение задачи изменено
  • Статус изменился, когда задача перешла на другой этап рабочего процесса типа задачи
  • Связанная задача переносится на тот же экземпляр JIRA
  • Участник запроса(Request) добавлен в задачу
  • Организация добавлена в задачу
  • Требуется одобрение, когда задача переводится на этап утверждения в рабочем процессе
  • Оставшееся время SLA, выберите SLA и статус цели, который запускает событие

 

 

 

 

 

 

 

 

 

IF

 

 

 

 

 

 

 

 

 

Позволяет указать условия для фильтрации выполнения правила автоматизации в зависимости от того, выполнено условие или нет.

По умолчанию JIRA Service Desk отправит несколько условий IF для вас, в зависимости от выбранного вами триггера WHEN, включая:

  • Задача соответствует определенному фильтру
  • Тип пользователя - клиент или агент.
  • Видимость комментария - внутренняя или внешняя
  • Комментарий содержит ключевую фразу
  • Комментарий - это основное действие, а не следствие другого действия (например, комментирование как часть перехода рабочего процесса)
  • Изменение разрешения либо установлено, либо очищено
  • Изменение статуса видимо для клиента
  • Тип ссылки соответствует определенному типу ссылки (например, связан или блокируется)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

THEN

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Позволяет указать, какие действия должны выполняться, когда выполняется правило автоматизации, и если условия выполняются.

 

 

 

 

 

 

 

 

 

 

По умолчанию JIRA Service Desk отправит вам несколько  действий ТОГДА, в том числе:

  • Переход задачи для изменения своего положения в рабочем процессе
  • Добавление комментария, как внутреннего, так и внешнего
  • Вызов пользователя для запроса конкретного пользователя или пользователей через @mention
  • Редактирование типа запроса для изменения типа запроса (поскольку типы запросов сопоставляются с конкретными типами задач, автоматизация не может изменять типы задач. Убедитесь, что ваши типы запросов являются одним и тем же типом задачи перед применением этого правила)
  • Измените задачу, чтобы выбрать и изменить поле в своей задаче, такое как представитель или приоритет (это влияет на поля, которые могут не отображаться в каждом типе задачи)
  • Отправить электронное письмо, чтобы создать уведомление по электронной почте
  • Webhook для отправки запроса POST (см. наш учебник)

Вот как это выглядит при настройке правила автоматизации:

РИСУНОК

После завершения этого урока вы сможете реализовать свои собственные компоненты «когда», «если» и «затем», чтобы расширить возможности автоматизации, чтобы делать практически все, что вы можете придумать, будь то SMS-уведомления после определенных действий , или интеграция с внешними системами через вызовы REST.

Об этих инструкциях

Вы можете использовать любую поддерживаемую комбинацию ОС и IDE для создания этого плагина. Эти инструкции были написаны с использованием IntelliJ IDEA на Mac OS X. Если вы используете другую комбинацию ОС или IDE, вы должны использовать эквивалентные операции для своей конкретной среды.

Этот учебник был последний раз проверен с JIRA 7.2.10 и Service Desk 3.2.10

Шаг 1. Создайте проект плагина

На этом этапе вы будете использовать Atlassian Plugin SDK для создания лесов для вашего проекта плагина. Atlassian Plugin SDK автоматизирует большую часть разработки плагинов для вас. Он включает команды для создания плагина и добавления модулей в плагин.

 Atlassian Plugin SDK

Вы можете сделать больше, чем просто создавать плагины с помощью SDK плагина. Для получения дополнительной информации см. Atlassian Plugin SDK.

  1. Если вы еще не настроили SDK Atlassian Plugin, сделайте это сейчас: настройте SDK Atlassian Plugin и создайте проект.
  2. В каталоге, куда вы хотите поместить проект плагина, введите следующую команду SDK:

atlas-create-jira-plugin

  1. Выберите 1 для JIRA 5, когда появится вопрос, в какой версии JIRA вы хотите создать плагин.
  2. В ответ на запрос введите следующую информацию, чтобы определить ваш плагин:

group-id

com.atlassian.plugins.tutorial.servicedesk

artifact-id

servicedesk-automation-extension

version

1.0.0-SNAPSHOT

package

com.atlassian.plugins.tutorial.servicedesk

  1. Подтвердите свои записи при появлении запроса.

SDK завершает работу и создает для вас каталог с исходными файлами проекта, включая POM (файл определения объектной модели проекта), исходный код заглушки и ресурсы.

Шаг 2. Просмотрите и настройте сгенерированный код заглушки

Это хорошая идея, чтобы ознакомиться с файлом конфигурации проекта, известным как POM (файл определения объектной модели проекта). POM определяет общие настройки для проекта, включая зависимости проекта и настройки сборки.

SDK генерирует и поддерживает POM самостоятельно, по большей части. Однако вам необходимо настроить некоторые из включенных метаданных для вашего проекта вручную, а именно:

  1. Перейдите в каталог проекта, созданный SDK, и откройте файл pom.xml для редактирования.
  2. Добавьте название своей компании или организации и ваш сайт в качестве значений name и url элемента organization:

<organization>
    <name>Example Company</name>
    <url>http://www.example.com/</url>
</organization>

  1. Обновите элемент description :

<description>Adds a new when, if and then rule component to JIRA Service Desk's automation feature.</description>

  1. Сохраните файл
  2. Теперь откройте файл atlassian-plugin.xml, созданный в каталоге src / main / resources, и удалите элементы <web-resource>, <component> и <component-import>. Нам это не нужно, поскольку мы просто добавляем реализации компонентов автоматизации. После того, как вы это сделали, ваш atlassian-plugin.xml должен выглядеть примерно так:

<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>

Шаг 3. Настройте Atlassian Spring Scanner

Atlassian Spring Scanner позволяет нам подключаться к зависимостям OSGi через аннотации более удобным способом, чем старая конфигурация на основе XML.

 

  1. Добавьте компиляцию Atlasian Spring Scanner и временные зависимости к корневому pom.xml:

<dependency>
    <groupId>com.atlassian.plugin</groupId>
    <artifactId>atlassian-spring-scanner-annotation</artifactId>
    <version>${atlassian.spring.scanner.version}</version>
    <scope>provided</scope>
</dependency>

  1. Не забудьте добавить свойство версии:

<atlassian.spring.scanner.version>2.0.1</atlassian.spring.scanner.version>

  1. Добавьте плагин Atlasian Spring Scanner Maven в <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>

  1. Наконец, мы должны сказать Spring сканировать аннотации, определяя контекст приложения. Создайте файл с именем spring.xml в src / main / resources / META-INF / spring со следующим содержимым:

<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>

Вот и все - теперь вы можете подключаться к зависимостям OSGi с помощью аннотаций. Мы сделаем это на более позднем этапе.

Шаг 4. Создайте обработчик ‘when’ «когда»

Мы собираемся создать обработчик «когда», который прослушивает, когда изменяет представитель запрос службы поддержки. Это позволит пользователям Service Desk создавать правило автоматизации, которое что-то делает, когда представитель обновляется.

4.1: Определите модуль

Добавьте в файл atlassian-plugin.xml, который вы изменили ранее:


<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>

Здесь определено несколько различных свойств. Вот что они представляют:

Свойство

Назначение

key

Это уникальный ключ модуля вашего обработчика. Этот ключ не может быть таким же, как и любой другой модуль, поэтому рекомендуется включать в него название вашей компании или какой-либо другой дифференциатор.

name

Не-интернационализированное имя вашего обработчика. Имя - это то, что отображается конечным пользователям в пользовательском интерфейсе.

 

Если не указано name-i18n-key, или если свойство i18n не существует, отображаемое имя в пользовательском интерфейсе автоматизации вернется к тому, что определено здесь.

name-i18n-key

Ключ i18n для имени вашего обработчика. Этот ключ должен ссылаться на свойство, определенное в некотором файле .properties в файле src / main / resources / i18n.

icon-class

Это определяет, какой значок будет использоваться для представления вашего обработчика. Чтобы узнать, какие классы сопоставляются с значками, просмотрите эту страницу.

unique *

Определяет, можно ли добавить несколько экземпляров обработчика в одно правило или нет. Значение по умолчанию false.

* Это свойство применимо только в Service Desk Server 3.1.x и Service Desk Cloud 3.2.0-OD-06.

 

 

 

 

 

 

 

 

 

 

provides

Это определяет, какие известные элементы информации этого, когда обработчик будет предоставлять ifs и thens, когда часть правила. Возможные варианты:

  • задача - задача JIRA будет передана в ifs и thens.
  • пользователь - пользователь JIRA будет передан в ifs и thens.
  • комментарий - комментарий, вызывающийся, когда обработчик будет передан в ifs и thens.

Впоследствии при определении ifs и thens вы можете указать, какой из этих элементов информации вам требуется, используя <require>. Это отношение «обеспечивает» и «требует» влияет на создание правил двумя способами:

  • Когда вы начинаете с нового правила, когда пользователь выбирает обработчик, ifs и thens, которые «требуют» вещи, когда обработчик не «предоставляет», не будут отображаться в пользовательском интерфейсе.
  • Для существующего правила, если в обработчике, когда обработчик изменен на то, что больше не удовлетворяет «требованиям» ifs и / или thens, пользователю будет показана ошибка проверки.

В нашем учебном примере мы говорим, что наш обработчик предоставит пользователю user(который будет новым представителем) и issue  задачу (задачу, чей представитель изменил).

РИСУНОК

 

Увидев, что мы только что определили ключ i18n для нашего обработчика when, теперь самое подходящее время для определения действительного свойства, на которое он ссылается. В разделе src / main / resources создайте новый каталог с именем i18n. Теперь в этом каталоге создайте файл с именем servicedesk-automation-extension.properties со следующим содержимым:


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

4.2: Определите обработчик события

Пока все, что мы сделали, это определение некоторых метаданных для нашего обработчика when. Нам еще нужно написать и подключить код, который фактически проверяет, изменился ли представитель задачи, и предоставить пользователю и задаче, если она есть. Для этого нам нужно создать новый класс, который реализует EventWhenHandler. Этот интерфейс определяет следующее соглашение:


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

getEventClass () возвращает тип события JIRA, когда handleEvent () вызывается всякий раз, когда происходит событие такого типа. Поскольку мы хотим проверить, изменился ли представитель * задачи *, мы заинтересованы в IssueEvents, поэтому давайте создадим для этого реализацию.

Во-первых, нам нужно сделать автоматические SPI (такие как EventWhenHandler) и API-интерфейсы доступными нам, добавив их как зависимость Maven в разделе <dependencies> корневого файла pom.xml. Нам также нужно добавить некоторые другие зависимости, которые мы будем использовать при создании нашего обработчика:


<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>

Не забудьте добавить соответствующие номера версий в <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>

Затем нам нужно импортировать точные интерфейсы из JIRA и плагин автоматизации, которые мы будем использовать. Мы будем следовать шаблону их определения в одном файле Java. Этот файл может быть расположен в любом пакете под src / main / java и должен содержать следующее:


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

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

/**
 * Этот класс используется для замены & lt; component-import / & gt; объявления в atlassian-plugin.xml. 
* Этот класс будет сканироваться сканером atlassian во время компиляции. 
* Нет ситуаций, когда вам когда-либо понадобится создать этот класс, он здесь исключительно для того, чтобы весь -импорт компоненты 
* находился в одном месте и не был разбросан по всему коду.
 */
@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("Этот класс не должен быть создан");
    }
}

Наконец, нам нужно OSGi импортировать пакеты SPI и API автоматизации (и другие, требуемые Spring Scanner), добавив <instructions> в <configuration> конфигурации maven-jira-plugin в корневом файле 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>

Полное определение <plugin> должно выглядеть так:


<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>

Теперь, когда мы определили зависимости Maven и OSGi, мы можем записать нашу реализацию EventWhenHandler для проверки того, изменился ли представитель задачи. Вот полная реализация 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;
    }
}

Этот класс реализует handleEvent (), который вызывается всякий раз, когда запускается IssueEvent. В методе выясняется, изменился ли представитель задачи. Если он не изменился, он возвращает пустой список, указывающий, что выполнение правила не должно распространяться дальше на ifs и actions. Если он изменился, он выполняет следующие действия:

  1. Создает сообщение правила. Это сообщение содержит контекстуальную информацию, которая передается в ifs и thens. Им нужна эта информация для выполнения своей работы. Когда мы определили модуль обработчика, мы сказали, что предоставляем задачу issue и пользователя user, поэтому мы должны * добавить их в сообщение правила.
  2. Создает список выполнения правил, которые должны иметь место. Выполнение правила должно иметь место для каждого правила, которое имеет это при настройке обработчика. Сообщение правила, созданное ранее, добавляется к каждому исполнению правила. При создании этого списка выполняется проверка каждого из них, когда конфигурация обработчика является частью правила, чтобы убедиться, что она предназначена для правильного проекта и что настроенный пользователь имеет разрешения для просмотра задачи, чей представитель изменился.
  3. Возвращает список выполнения правил. Затем каждый из них выполняется либо асинхронно, либо в одном потоке. Независимо от того, является он синхронным или нет, определяется методом requestSynchronousExecution (boolean) RuleExecutionCommandBuilder. В AssigneeChangedEventWhenHandler мы попросили, чтобы каждое выполнение правила выполнялось асинхронно:

final RuleExecutionCommandBuilder ruleExecutionStub = ruleExecutionCommandBuilderService.builder()
        .requestSynchronousExecution(false)
        .ruleMessage(messageForIfsAndThens);

AssigneeChangedEventWhenHandler использует многочисленные вспомогательные классы и службы, которые предоставляются плагином автоматизации. ProjectAndUserChecker - это исключение, это то, что вам нужно написать самостоятельно. Вот реализация, которую мы используем в учебнике:


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);
            }
        });
    }
}

Теперь, когда мы определили наши классы обработчиков, последний шаг - добавить событие, когда определение обработчика в наш 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>

Обратите внимание, что это необходимо в дополнение к тому, когда ранее было добавлено определение обработчика. Убедитесь, что значение ключа модуля совпадает с  ‘key’ «ключом» вышеприведенного определения обработчика. Теперь ваш полный atlassian-plugin.xml должен выглядеть так:


<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: Проверьте, что ваш обработчик доступен для использования

Выполните шаги 2-6 потока разработки плагина JIRA Service Desk. После установки плагина в вашем исполняемом экземпляре JIRA откройте вкладку «Настройки» в любой службе поддержки (создайте службу поддержки, если она еще не существует) и выберите «Автоматизация».

РИСУНОК

Добавьте правило автоматизации, используя «Пользовательское правило» в качестве шаблона. Теперь вы должны увидеть новую опцию «Редактируемый выпуск» при настройке правила WHEN.

РИСУНОК

Это мило, но давайте посмотрим, действительно ли это работает. Создайте полное правило, в качестве «триггера» «представитель задачи изменен ». Давайте добавим комментарий, когда это произойдет, для любой задачи:

РИСУНОК

Теперь сохраните правило и давайте его протестируем. Чтобы на самом деле изменить представителя задачи, в вашем экземпляре JIRA должно быть как минимум два пользователя, каждый из которых имеет доступ к проекту, для которого вы создали правило автоматизации. Вам нужно будет добавить второго пользователя в качестве агента в Service Desk через вкладку «Люди».

Создайте или откройте задачу в том же проекте, для которого вы только что создали правило автоматизации. Теперь измените представителя. Вы должны увидеть комментарий, добавленный автоматически к задаче JIRA. Приятной работы!

Шаг 5. Создайте условие «если»

Итак, теперь мы можем определить, изменился ли представитель задачи. В каких условиях мы хотим выполнить действие, когда это произойдет? Как насчет того, имеет ли человек, который изменил представителя, адрес электронной почты, принадлежащий определенному домену? Это может быть полезно, если, например, вы хотите выполнить действие только в том случае, если пользователь, который изменяет представителя, принадлежащего внешней компании.

5.1: Определите модуль

Добавьте в файл atlassian-plugin.xml следующее:


<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>

Как и наше определение модуля обработчика, мы определяем свойства ключа key, имени name, name-i18n-key и значка icon-class. См. «Определение модуля» в разделе компонента «Обработчик» для получения дополнительной информации об этом. Следующие свойства, с которыми мы не сталкивались раньше:

Свойство

Назначение

requires

Это аналогично предоставлению provides в нашем определении обработчика и определяет список вещей, которые мы требуем для того, чтобы этот компонент правила был доступен для выбора. Чтобы проверить домен адреса электронной почты пользователя, вызвавшего выполнение правила, нам нужен только пользователь, так что все, что мы здесь определяем.

 

См. «Определение модуля» в разделе компонента обработчика для других вещей, которые мы могли бы добавить здесь.

visualiser

По умолчанию любая меткаопределяемая именем (или значением свойства i18n-name-key), отображается в пользовательском интерфейсе для компонента правила. Например, у нашего обработчика «имя представителя задачи» изменено как его имя, и так выглядит в пользовательском интерфейсе:

РИСУНОК

Однако, что, если мы хотим, чтобы эта метка менялась в зависимости от конфигурации компонента? В нашем случае было бы намного легче прочитать правило, если бы наш ярлык был «Пользовательский домен электронной почты - это«atlassian.com », а не просто «Пользовательский домен электронной почты ». Это то, что делает визуализатор: он позволяет определить, как должна выглядеть метка компонента правила в зависимости от конфигурации.

Созданный вами класс визуализатора должен реализовать интерфейс RuleComponentVisualiser, который определяет контракт следующим образом:


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

Определение визуализатора необязательно. Если вы его не определяете или пользователь еще не добавил какую-либо конфигурацию, используется имя (или значение свойства ключа i18n-name-key).

validator

Валидатор, который вы определяете здесь, будет вызываться для вашего компонента до того, как будет сохранено правило и до того, как будет загружено правило. Если возвращается результат отказа , то либо все правило не будет сохранено, либо оно будет загружено с ошибкой, выделенной для вашего компонента.

Созданный вами класс валидатора должен реализовать интерфейс IfConditionValidator, который определяет следующий контракт:


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

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

Определение валидатора для вашего компонента правила является необязательным. Если вы его не определите, компонент всегда будет проходить проверку(валидацию).

web-form-module

Это модуль AMD, который определяет внешний вид и логику вашего компонента правила. Этот модуль обрабатывает такие вещи, как макет пользовательского интерфейса, проверку на стороне клиента и рендеринг сообщений об ошибках.

Мы определили ключ i18n для нашего условия if, поэтому мы также должны определить фактическое свойство, на которое это ссылается. В файле src / main / resources / i18n / servicedesk-automation-extension.properties, созданном ранее, добавьте следующую строку:


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

5.2: Определите условие if

Теперь, когда мы определили метаданные условия if, давайте реализуем сам класс условия if. Как уже упоминалось ранее, этому классу необходимо реализовать интерфейс IfCondition. Вот полная реализация 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 matches(final IfConditionParam ifConditionParam)
    {
        // Get the email domain we want to check for
        final Option 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 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
        );
    }
}
,>,>

Когда выполняется правило, которое настроено с доменом электронной почты пользователя  условия "если", вызывается метод matches (). IfConditionParam содержит любую контекстуальную информацию, необходимую условию if для выполнения своей работы, в том числе для любого домена электронной почты, который был настроен для проверки. Домен электронной почты хранится на карте данных data, содержащихся в карте данных data ifConditionParam. Эта карта содержит все данные, настроенные для домена электронной почты пользователя, условия "если" в определенном правиле.

Мы извлекаем значение домена электронной почты с карты, используя ключ «email.domain». Это то, что мы будем хранить при значении, когда мы определяем front-end нашего компонента правила.

Как только у нас есть домен электронной почты для проверки, следующая вещь, которую выполняет наша реализация match (), - это получение адреса электронной почты (и, следовательно, домена) для пользователя, вызвавшего обращение к начальному правилу. Если правило было настроено представителем изменившим обработчика, это будет пользователь, который изменил представителя. Этот пользователь сохраняется в предоставленном сообщении правила. Чтобы облегчить извлечение этого пользователя, мы используем UserMessageHelper, вспомогательный класс, предоставляемый модулем API автоматизации.

Теперь мы выполняем простую проверку, чтобы убедиться, что домен электронной почты пользователя соответствует ожидаемому домену, который был настроен, и возвратить результат.

Вы заметили в нашем UserEmailDomainIfCondition, что мы использовали IfConditionErrorHelper. Поскольку это то, что предоставляется другим плагином, последнее, что нам нужно сделать, это добавить следующую строку в наш файл GeneralOsgiImports:


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

Если мы забудем это сделать, наша реализация условия if завершится с ошибкой NullPointerException.

5.3: Определение визуализатора

Как упоминалось ранее, визуализатор используется для определения логики, которая определяет, какое имя и метка отображаются для компонента правила. Это можно сделать динамически, на основе конфигурации компонента правила. Название компонента компонента и метка отображаются в следующих местах:

РИСУНОК

Для этого мы создаем класс, реализующий RuleComponentVisualiser, и добавляем логику для возврата имени и метки при задании конфигурации. Ниже приведена полная реализация нашего примера домена электронной почты пользователя:


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 getLabel(@Nonnull final RuleComponentVisualiserParam ruleComponentVisualiserParam)
    {
        final Option 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);
        }
    }
}

Этот класс использует I18nHelper, чтобы гарантировать, что наш компонент правил  i18n готов к работе. Нам просто нужно определить новое свойство, которое мы ссылаемся в нашем файле src / main / resources / servicedesk-automation-extension.properties, добавив следующую строку:


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

Последним важным шагом, который мы не можем забыть, является добавление оператора импорта компонентов для I18nHelper, который мы используем для GeneralOsgiImports, поскольку этот класс предоставляется нам из другого плагина:


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

5.4: Определение валидатора

Метод validate () нашего валидатора вызывается, когда правило, в котором находится наш компонент, загружается или сохраняется, и отвечает за определение того, находится ли конфигурация нашего компонента правила в состоянии, с которым может работать наше условие.

В нашем UserEmailDomainIfConditionValidator мы проверяем, что введенный домен электронной почты присутствует и действителен (для нашего простого руководства «действительный» означает «не содержит символ @»). Вот полная реализация:


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 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> errorList = newHashMap();
        errorList.put(EMAIL_DOMAIN_FIELD_NAME, newArrayList(i18nHelper.getText(errorI18nKey)));

        return ValidationResult.FAILED(errorList);
    }
}
,>

В нашем валидаторе вы заметите, что мы определили два новых свойства ошибки, поэтому давайте добавим их в 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: Напишите интерфейсные ресурсы

Ранее в нашем atlassian-plugin.xml мы определили наш веб-модуль формы как servicedesk / settings / automation / tutorial / modules / ruleif / useremaildomain-if-condition-form. Теперь нам нужно создать файлы шаблона Soy и Javascript, которые составляют этот модуль веб-формы.

Во-первых, давайте создадим место, в которое мы поместим эти файлы, и скажем системе плагинов, где они могут их найти. Создайте следующий каталог в разделе src / main / resources:


servicedesk/settings/automation/tutorial/modules

Теперь добавьте следующее в atlassian-plugin.xml:


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

Значение context выше указывает плагин автоматизации, в котором части Service Desk должны загружать эти ресурсы. В нашем случае мы хотим, чтобы они использовались только при администрировании проектов.

Итак, теперь, когда наши интерфейсные ресурсы можно найти, давайте начнем создавать их для нашего условия if. Поскольку мы будем создавать ресурсы для нашего действия в следующей части этого руководства, давайте сохраним ресурсы нашего условия if в их собственной папке. Создайте один под src / main / resources / servicedesk / settings / automation / tutorial / modules с именем «rule if».

В этом каталоге мы собираемся создать четыре файла:

Файл

Назначение

useremaildomain-if-condition.js

Это содержит интерфейсную логику для:

  • Отображение нашего пользовательского интерфейса компонента правила
  • Проверка на стороне клиента
  • Ошибки рендеринга
  • Сериализация любого ввода перед отправкой на сервер

Именно здесь мы определим модуль AMD, на который ссылается atlassian-plugin.xml.

useremaildomain-if-condition.soy

Он содержит шаблон HTML, который будет отображаться внутри поля компонента правила. Здесь вы определяете фактические входные данные HTML-формы для своих данных, используя Soy. Для получения дополнительной информации о написании шаблонов сои см. это руководство.

useremaildomain-if-condition-model.js

Здесь мы определяем модель Backbone.js для наших данных компонента правила. В нашем случае данные будут содержать только одну строку домена электронной почты пользователя.

useremaildomain-if-condition-view.js

Здесь мы определяем любую пользовательскую логику пользовательского интерфейса для нашего компонента правила. В нашем случае не требуется пользовательская логика пользовательского интерфейса, поэтому наша реализация будет довольно легкой.

 

Технологический агностик

  В наших примерах мы используем Backbone.js и Soy, но вы не ограничены этими технологиями. Единственными ограничениями, наложенными на ваш внешний интерфейс, являются:

  • Вы должны определить модуль AMD с именем, указанным в вашем atlassian-plugin.xml.
  • Вы должны реализовать необходимые методы Javascript SPI:

render: function(config, errors)

Здесь вы возвращаете HTML-код вашего front-end, как строку Javascript. Мы используем Soy для этой цели в наших примерах, но вы можете использовать любой язык шаблонов на клиентской стороне, который вам нравится.

serialize: function ()

Здесь вы конвертируете пользовательский ввод в формат JSON для передачи в API автоматизации на стороне сервера.

 

Большинство интересных вещей происходит в useremaildomain-if-condition.js, поэтому давайте рассмотрим это более подробно:


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);
    };
}); 

Первое, что мы делаем, это включим определение модуля AMD, определяющее модуль servicedesk / settings / automation / tutorial / modules / ruleif / useremaildomain-if-condition-form, на который мы ссылаемся в atlassian-plugin.xml.

Затем мы создадим нашу реализацию SPI, хранящуюся в переменной userEmailDomainView. Эта функция создает HTML-код, используя наш шаблон Soy для этого. Он также использует методы JS API, предоставляемые автоматизацией, а именно: on(), of (), renderFieldError () (для отображения сообщений об ошибках для определенного поля) и renderGlobalError () (для отображения ошибок, не связанных с полем).

Наконец, этот метод реализует необходимые методы SPI render () и serialise (), а также реализует два дополнительных метода SPI:

validate: function (deferred)

Здесь вы включаете любую проверку на стороне клиента.

dispose: function()

Здесь вы перестаете слушать события и т. д.

Ниже вы можете найти полную реализацию других файлов, составляющих наши интерфейсные ресурсы. Для краткости я не буду подробно останавливаться на каждом из них.

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;
        }
    });
});

В нашем шаблоне Soy вы заметите, что мы ссылаемся на новую строку свойств, tutorial.if.condition.user.email.domain.prompt. Давайте определим это в нашем файле src / main / resources / servicedesk-automation-extension.properties:


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

Последнее, что нам нужно сделать, это добавить новый класс в GeneralOsgiImports. Когда мы обращаемся к строкам свойств в наших шаблонах Soy, за кулисами используется I18nHelper.BeanFactory, поэтому добавим:


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

5.6. Убедитесь, что он работает.

Прежде чем мы запустим JIRA и убедитесь, что мы можем создать правило с нашим новым условием if, давайте быстро проверим наши изменения в файлах, которые мы создали как часть предыдущих разделов. Вот как выглядят следующие файлы:

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  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 

Хорошо, теперь мы готовы проверить наши изменения.

Еще раз выполните шаги 2-6 потока разработки плагина JIRA Service Desk. После установки плагина в вашем исполняемом экземпляре JIRA откройте вкладку «Настройки» в любой службе поддержки (создайте службу поддержки, если она еще не существует) и выберите «Автоматизация».

РИСУНОК

Добавьте правило автоматизации, используя «Пользовательское правило» в качестве шаблона. Выберите «Представитель задачи изменен» для вашего КОГДА WHEN, затем нажмите на ваш IF. Теперь вы должны увидеть новую опцию «Пользовательский домен электронной почты» при настройке IF для правила.

РИСУНОК

Хорошо, мы можем добавить его в правило, но снова давайте посмотрим, действительно ли он работает. Создайте полное правило с «доменом электронной почты пользователя» в качестве триггера. Выберите адрес электронной почты пользователя, с которым вы хотите протестировать:

РИСУНОК

Теперь, как и раньше, сделайте наш ТОГДА THEN комментарием, который делает очевидным правило:

РИСУНОК

Теперь давайте дадим правилу имя и сохраним его, затем давайте протестируем его. Чтобы действительно проверить наше условие if, вам нужно иметь двух пользователей в вашем экземпляре JIRA: тот, у которого есть указанный вами домен, и тот, у которого его нет. У каждого из этих пользователей должен быть доступ к проекту, для которого вы создали правило автоматизации. Вам нужно будет добавить второго пользователя в качестве агента в Service Desk через вкладку «Люди».

Войдите как пользователь, у которого есть адрес электронной почты, соответствующий указанному домену. Создайте или откройте задачу в том же проекте, для которого вы только что создали правило автоматизации. Теперь измените представителя. Вы должны увидеть комментарий, добавленный автоматически к задаче JIRA.

Теперь войдите в систему как пользователь, у которого нет соответствующего домена адреса электронной почты. Выполните те же действия, что и выше. На этот раз вы не должны видеть комментарий, добавленный в билет. Условие if предотвратило выполнение этого правила, а затем действие. Отличная работа!

Шаг 6. Создайте Ваш 'тогда' действие

После всей нашей тяжелой работы пока, это теперь возможно для нас, чтобы сказать, изменена ли задача своего представителя пользователем от внешней компании (мы предполагаем, что все используют их рабочий адрес электронной почты). Что мы хотим сделать с этой информацией? Как о мы добавляем метку к нашей задаче, таким образом мы можем категоризировать задачи, управляемые внешне. Давайте сделаем это, выполнив действие «Добавить ярлык», а затем действие.

Шаги по созданию нашего «тогда» ‘then’ действия идентичны созданию условия if: мы определяем наш модуль, определяем класс реализации действия, определяем наш визуализатор и валидатор и пишем наши внешние ресурсы.

6.1: Определите модуль

Добавьте ниже: 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>

Вы столкнулись со всеми этими свойствами на предыдущих шагах. Как и в предыдущих шагах, мы определили новое свойство, которое определяет или имя модуля, поэтому добавим это в src / main / resources / i18n / servicedesk-automation-extension.properties:


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

6.2: Определить действие then

Этот класс выполняет фактическую работу по добавлению метки к задаче JIRA и должен реализовать интерфейс ThenAction, который определяет следующий контракт:


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

Когда выполнение правила достигло своего компонента action, invoke () вызывается с помощью ThenActionParam, содержащего весь контекст, необходимый для реализации действия then, чтобы выполнить его работу. В нашем случае этот параметр будет содержать задачу JIRA, к которой мы хотим добавить метку, а также настроенную метку для добавления.

Вот наша полная реализация 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 invoke(final ThenActionParam thenActionParam)
    {
        // Get the label we want to add to the issue
        final Option 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 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 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 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 errorIter = errorCollection.getErrorMessages().iterator(); errorIter.hasNext();)
        {
            errorMessage.append(errorIter.next()).append(";");
            if(errorIter.hasNext())
            {
                errorMessage.append(" ");
            }
        }

        return errorMessage.toString();
    }
}
,>,>

Здесь вы заметите, что мы использовали LabelService, предоставленный JIRA, чтобы добавить метку к этой задаче. Поскольку это предусмотрено вне нашего плагина, мы должны добавить следующее в GeneralOsgiImports:


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

Мы также используем ThenActionErrorHelper, который предоставляется другим плагином, поэтому мы должны добавить следующее в GeneralOsgiImports:


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

6.3: Определить визуализатор

Точно так же, как и в нашем условии if, визуализатор позволяет нам контролировать, что отображается в пользовательском интерфейсе правила, когда настроен наш компонент действия. Естественно, мы хотим отобразить метку, которая будет добавлена к задачам для нашей метки задач, а затем действия. Мы реализуем тот же интерфейс, что и для нашего условия if, а именно RuleComponentVisualiser. Вот полная реализация:


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: Определите валидатор

Наш валидатор реализован идентично нашему валидатору if, за одним исключением: класс реализации должен реализовать интерфейс ThenActionValidator. Вот полная реализация:


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);
    }
}
 

Мы придерживаемся проверки довольно простой для этого учебника - мы просто проверяем, что пользователь что-то набрал.

Мы добавили новое свойство строки, чтобы указать недостающее значение метки, поэтому добавим это в src / main / resources / servicedesk-automation-extension.properties, как обычно:


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

6.5: Запишите внешние ресурсы

Опять же, это будет очень похожий процесс на то, как мы создали наши внешние интерфейсы if. Во-первых, давайте создадим место для того, чтобы наши исходные ресурсы для этого действия существовали. Создайте каталог «rulethen» в разделе src / main / resources / servicedesk / settings / automation / tutorial / modules.

Как и в случае с нашим условием if, в этом каталоге мы создадим следующие файлы:

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

 

После определения наших интерфейсных ресурсов if, здесь нет ничего нового, поэтому я буду держать кратким и обеспечить полную реализацию этих файлов, не вдаваясь в подробности:

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;
        }
    });
});

Наши шаблоны Soy ссылаются на tutorial.then.action.issue.label.prompt, поэтому добавим это в src / main / resources / servicedesk-automation-extension.properties:


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

6.6. Убедитесь, что он работает.

Теперь мы готовы проверить, что наш метка, а затем действие работают должным образом. Прежде чем мы это сделаем, давайте быстро проверим изменения, которые мы внесли в существующие файлы. Вот как они теперь должны выглядеть:

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


    @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

Теперь давайте попробуем добавить метки к нашим задачам через наш новый компонент правила действия.

В третий раз выполните шаги 2-6 потока разработки плагина JIRA Service Desk. После установки плагина в вашем исполняемом экземпляре JIRA откройте вкладку «Настройки» в любой службе поддержки (создайте службу поддержки, если она еще не существует) и выберите «Автоматизация».

РИСУНОК

Добавьте правило автоматизации, используя «Пользовательское правило» в качестве шаблона. Выберите «Представитель задачи изменен» для вашего WHEN(Когда) и определенного домена электронной почты для вашего IF. Теперь вы должны увидеть новую опцию «Добавить метку для задачи» при настройке  правила THEN (Тогда).

РИСУНОК

Добавление его к правилу - все вполне и хорошо, но если он не выполняет то, что он обещает, это бесполезно. Создайте полное правило, используя все компоненты, которые мы создали:

  • «Представитель задачи изменен» для КОГДА WHEN
  • «Пользовательский домен электронной почты» - это «<ваш тестовый домен>» для IF'
  • Добавить метку' 'external' к задаче для WHEN:

РИСУНОК

Теперь дайте правилу имя и сохраните его. Как и прежде, в вашем экземпляре JIRA вам нужно будет иметь двух пользователей: тот, у которого есть домен электронной почты, указанный вами в условии if, и тот, у которого эго нет. У каждого из этих пользователей должен быть доступ к проекту, для которого вы создали правило автоматизации. Вам нужно будет добавить второго пользователя в качестве агента в Service Desk через вкладку «Люди».

Барабанная дробь

Войдите как пользователь, у которого есть адрес электронной почты, соответствующий указанному домену. Создайте или откройте задачу в том же проекте, для которого вы только что создали правило автоматизации. Теперь измените цессионария. Вы должны увидеть метку, добавленную в билет так:

РИСУНОК

Теперь войдите как пользователь, который * не * имеет соответствующего домена адреса электронной почты. Выполните те же действия, что и выше, за исключением новой задачи. На этот раз вы не должны * видеть метку, добавленную в билет. Добились ! Все наши компоненты правил прекрасно работают вместе, и теперь у нас есть супер эффективное правило автоматизации добавления меток, когда внешняя компания меняет представителя задачи:

РИСУНОК

Не только это, но каждый отдельный компонент правила, который вы создали, может использоваться для составления других правил. Удивительно, а?

Пойдите и выпейте чашку чая. Вы заслужили это.

 

По материалам Atlassian JIRA  Server Developer Creating automation rule components