Написание пользовательского импортера с использованием надстройки импортеров JIRA

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

Это руководство относится к JIRA 5.0.2 или новее с установленным плагином импортеров JIRA  5.0.2 или более поздней версией.

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

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

Сроки:

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

 

Обзор этого руководства

Надстройка Atlassian JIRA Importers (JIM) позволяет администраторам импортировать задачи из внешних трекеров задач в JIRA. Надстройка JIM поставляется вместе с JIRA по умолчанию, но вы можете дополнить его своими собственными  пользовательскими импортерами. Это позволяет вам создавать импортеры для проприетарных систем отслеживания задач, скажем, или для импорта задач из любого типа системы каким-либо обычным способом.

В этом руководстве показано, как создать пользовательский импортер. Мы создадим плагин, который импортирует задачи из файла значений разделенных запятыми в JIRA. Это простой пример и тот, который в основном дублирует импортер уже в JIM, но он показывает основные шаги, связанные с созданием собственного импортера.

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

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

Этот учебник был последний раз проверен с JIRA 6.0.4.

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

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

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

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

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

Чтобы клонировать репозиторий в вашей системе, введите следующую команду:


git clone https://bitbucket.org/atlassian_tutorial/tutorial-jira-simple-csv-importer.git

Кроме того, вы можете загрузить исходный код в виде ZIP-архива, выбрав «Загрузки», а затем «Филиалы»(Ветви): https://bitbucket.org/atlassian_tutorial/tutorial-jira-simple-csv-importer

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

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

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

atlas-create-jira-plugin

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

group-id

com.example.plugins.tutorial.jira.csvimport

artifact-id

simple-csv-importer

version

1.0-SNAPSHOT

package

com.example.plugins.tutorial.jira.csvimport

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

SDK создает домашний каталог вашего проекта с исходными файлами проекта, исходным кодом заглушки и ресурсами плагина.

Шаг 2. Измените POM

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

  1. Откройте POM-файл (pom.xml) для редактирования. Вы можете найти его в корневой папке вашего проекта.
  2. Добавьте название организации или веб-сайт в элемент организации organization:

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

  1. Затем добавьте в свой POM следующие свойства зависимости и версии:

<dependencies>
...
    <dependency>
        <groupId>com.atlassian.jira.plugins</groupId>
        <artifactId>jira-importers-plugin</artifactId>
        <version>${jim.version}</version>
        <scope>provided</scope>
    </dependency>
...
</dependencies>
...
<properties>
...
    <jim.version>6.0.11</jim.version>
...
</properties>

Здесь мы добавляем плагин  импортеров JIRA  в качестве зависимости. Поскольку мы установили объем этой зависимости к provided, плагин полагается на пакет JIM, который поставляется с JIRA во время выполнения. Использование другой области действия, вероятно, вызовет задачи с загрузчиками классов, связанные с дублирующимися классами, доступными в пути класса plugin. Версия JIM, указанная нами, 6.0.11, совпадает с версией JIRA 6.0.4 по умолчанию.

  1. Если вы используете версию JIRA, у которой нет JIM 5.0.2 или выше (JIRA версии 5.2 или более ранняя версия), вы также должны установить свойство plugins в свой pom.xml. Это позволяет SDK устанавливать указанную версию JIM при запуске JIRA.

<properties>
...
    <plugins>com.atlassian.jira.plugins:jira-importers-plugin:${jim.version}</plugins>
...
</properties>

  1. Сохраните ваши изменения.

 

Шаг 3. Добавьте модуль импортера внешней системы

Начиная с версии JIM 5.0 разработчики плагинов могут создавать импортеры, объявляя модуль плагина external-system-importer в дескрипторе плагина.

Давайте добавим его в наш дескриптор плагина:

  1. Откройте редактор плагинов, atlassian-plugin.xml, для редактирования. Файл дескриптора плагина находится в директории src / main / resources вашего дома проекта.
  2. Добавьте объявление external-system-importer в качестве дочернего элемента в элемент atlassian-plugin:

    <external-system-importer name="SimpleCSVImporter" key="SimpleCSVImporterKey"
        i18n-description-key="com.example.plugins.tutorial.jira.csvimport.description"
        i18n-supported-versions-key="com.example.plugins.tutorial.jira.csvimport.versions"
        logo-module-key="com.example.plugins.tutorial.jira.csvimport.simple-csv-importer:graphics"
        logo-file="simplecsv.png"
        class="com.example.plugins.tutorial.jira.csvimport.SimpleCsvImporterController"
        weight="20"/>

Атрибутами элемента являются:

  • key - это ключ плагина для этого импортера.
  • i18n-name-key - это необязательный ключ i18n для имени модуля.
  • i18n-description-key - строка описания i18n для этого импортера.
  • i18n-support-versions-key - это необязательное сообщение i18n, указывающее версии JIRA, которые поддерживает этот импортер.
  • logo-module-key - это имя веб-ресурса, содержащего графику, которую вы хотите использовать для этого импортера. Это значение составлено из полнофункционального ключа плагина для этого плагина (идентификатор группы и идентификатор артефакта, который вы вводили при создании проекта), плюс атрибут имени из модуля веб-ресурсов (который мы добавляем на следующем шаге).
  • logo-file - это имя файла логотипа в элементе веб-ресурса, определяемом logo-module-key.
  • class - это полное имя класса, которое реализует интерфейс AbstractImporterController. Для нашего импортера класс будет com.example.plugins.tutorial.jira.csvimport.CsvImporterController.
  • weight - это числовое значение, которое определяет положение этого импортера в списке внешних импортеров, с ниже пронумерованными импортерами, появляющихся более ранними в списке. Встроенные импортеры имеют весовые значения от 1 до 50. Не стесняйтесь экспериментировать с размещением импортера на странице, изменяя это число. Например, делая его 0(нулевым) вы помещаете его вторым в списоке, так как уже есть импортер с весом 0.
  1. Добавьте элемент web-resource, который идентифицирует графику, которая будет использоваться в качестве значка для нашего пользовательского импортера на странице «Внешние импортеры»

<web-resource key="graphics" name="Importer Graphics">
   <resource type="download" name="simplecsv.png"         
       location="images/simplecsv.png"/>
</web-resource>

  1. Импортируйте несколько внешних компонентов в наш плагин, добавив эти инструкции для component-import

<component-import key="jiraDataImporter"
       interface="com.atlassian.jira.plugins.importer.imports.importer.JiraDataImporter"/>
<component-import key="usageTrackingService"
       interface="com.atlassian.jira.plugins.importer.tracking.UsageTrackingService"/>

Это импортирует два компонента из JIM:

 

  • JiraDataImporter отвечает за обработку сквозного процесса импорта.
  • UsageTrackingService позволяет отслеживать использование вашего импортера. Для этого урока мы будем полагаться на стандартную службу отслеживания JIM. Однако вы можете реализовать свое собственное отслеживание, просто реализуя интерфейс UsageTrackingService.

Шаг 4. Настройте несколько ресурсов пользовательского интерфейса.

В каталоге ресурсов, давайте позаботимся о нескольких ресурсах для импортера. Каждый импортер появляется на странице «Внешние импортеры» с размером около 133x64 пикселей.

Давайте добавим один для нашего импортера и добавим строки пользовательского интерфейса:

  1. Загрузите следующее изображение и поместите его в каталог src / main / resources / images:

РИСУНОК

  1. Добавьте следующие свойства в файл src / main / resources / simple-csv-importer.properties.

com.example.plugins.tutorial.jira.csvimport.description=Simple CSV Importer
com.example.plugins.tutorial.jira.csvimport.versions=comma-separated value files
com.example.plugins.tutorial.jira.csvimport.step.oauth=Authentication
com.example.plugins.tutorial.jira.csvimport.step.csvSetup=Setup
com.example.plugins.tutorial.jira.csvimport.step.projectMapping=Projects
com.example.plugins.tutorial.jira.csvimport.step.customField=Custom Field
com.example.plugins.tutorial.jira.csvimport.step.fieldMapping=Field mapping
com.example.plugins.tutorial.jira.csvimport.step.valueMapping=Value mapping
com.example.plugins.tutorial.jira.csvimport.step.links=Link

Поля описания и версии отображаются на странице внешних импортеров. Другие поля будут отображаться как метки для отдельных шагов мастера импорта.

Шаг 5. Внедрите класс AbstractImporterController

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

Создайте класс контроллера импортера в com.example.plugins.tutorial.jira.csvimport.SimpleCsvImporterController. Дайте ему следующий код:


package com.example.plugins.tutorial.jira.csvimport;

import com.atlassian.jira.plugins.importer.imports.importer.ImportDataBean;
import com.atlassian.jira.plugins.importer.imports.importer.JiraDataImporter;
import com.atlassian.jira.plugins.importer.web.*;
import com.example.plugins.tutorial.jira.csvimport.web.SimpleCsvSetupPage;
import com.google.common.collect.Lists;
import java.util.List;

public class SimpleCsvImporterController extends AbstractImporterController {
    public static final String IMPORT_CONFIG_BEAN = "com.example.plugins.tutorial.jira.google.csvimport.config";
    public static final String IMPORT_ID = "com.example.plugins.tutorial.jira.google.csvimport.import";
    public SimpleCsvImporterController(JiraDataImporter importer) {
        super(importer, IMPORT_CONFIG_BEAN, IMPORT_ID);
    }
    @Override
    public boolean createImportProcessBean(AbstractSetupPage abstractSetupPage) {
        if (abstractSetupPage.invalidInput()) {
            return false;
        }
        final SimpleCsvSetupPage setupPage = (SimpleCsvSetupPage) abstractSetupPage;
        final SimpleCsvConfigBean configBean = new SimpleCsvConfigBean(new SimpleCsvClient(setupPage.getFilePath()));
        final ImportProcessBean importProcessBean = new ImportProcessBean();
        importProcessBean.setConfigBean(configBean);
        storeImportProcessBeanInSession(importProcessBean);
        return true;
    }
    @Override
    public ImportDataBean createDataBean() throws Exception {
        final SimpleCsvConfigBean configBean = getConfigBeanFromSession();
        return new SimpleCsvDataBean(configBean);
    }
    private SimpleCsvConfigBean getConfigBeanFromSession() {
        final ImportProcessBean importProcessBean = getImportProcessBeanFromSession();
        return importProcessBean != null ? (SimpleCsvConfigBean) importProcessBean.getConfigBean() : null;
    }
    // @Override no override annotation to remain compatible with JIRA 5
    public List<String> getStepNameKeys() {
        return Lists.newArrayList(
                "com.example.plugins.tutorial.jira.csvimport.step.csvSetup",
                "com.example.plugins.tutorial.jira.csvimport.step.projectMapping",
                "com.example.plugins.tutorial.jira.csvimport.step.customField",
                "com.example.plugins.tutorial.jira.csvimport.step.fieldMapping",
                "com.example.plugins.tutorial.jira.csvimport.step.valueMapping",
                "com.example.plugins.tutorial.jira.csvimport.step.links"
        );
    }
    @Override
    public List<String> getSteps() {
        return Lists.newArrayList(SimpleCsvSetupPage.class.getSimpleName(),
                ImporterProjectMappingsPage.class.getSimpleName(),
                ImporterCustomFieldsPage.class.getSimpleName(),
                ImporterFieldMappingsPage.class.getSimpleName(),
                ImporterValueMappingsPage.class.getSimpleName(),
                ImporterLinksPage.class.getSimpleName());
    }
    @Override
    public boolean isUsingConfiguration() {
        return false;
    }
}

Обратите внимание, что контроллер импорта реализует абстрактный класс AbstractImporterController. Среди базовых методов, которые мы переопределяем, мы имеем:

  • createImportProcessBean () создает экземпляр ImportProcessBean и AbstractConfigBean вместе с несколькими другими вещами, которые нам нужны. Конфигурационный компонент в этом случае - SimpleCsvConfigBean, который отвечает за обработку процесса настройки для нас. Он также отвечает за сохранение importProcessBean в сеансе (используя метод storeImportProcessBeanInSession ()). В нашем случае мы также создаем SimpleCsvClient, вспомогательный класс для инкапсуляции всей логики, связанной с нашей внешней системой. Мы должны поместить его здесь, так как мы не можем связывать пользовательские объекты с ImportProcessBean; вместо этого мы будем использовать SimpleCsvConfigBean для переноса этого объекта для нас. Обратите внимание, что сохранение importProcessBean в сеансе означает, что для каждого зарегистрированного пользователя может быть создан только один экземпляр импортера.
  • createDataBean () получает помощь от частного метода getConfigBeanFromSession (), чтобы вернуть компонент данных конфигурации, контролирующий процесс импорта для нашего пользовательского импортера.
  • getSteps () возвращает список имен действий, которые формируют шаги мастера для этого импортера. Здесь мы имеем:
    • SimpleCsvSetupPage предоставляет нашу страницу пользовательской настройки, которая считывает имя файла, предоставленное пользователем.
    • ImporterProjectMappingsPage - это страница повторного сопоставления проекта, которая позволяет пользователю выбирать или создавать проект, в который нужно импортировать проблемы.
    • ImporterCustomFieldsPage - многократно использованное пользовательское поле, отображающее страницу, которая позволяет пользователю отображать пользовательские поля от внешнего трекера задач до пользовательских полей JIRA.

 

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

 

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

 

Обратите внимание, что JIM автоматически добавляет конечный шаг для каждого импортера, поэтому нам не нужно было включать его здесь. Он показывает результаты события импорта. Также обратите внимание на вызов метода .class.getSimpleName (). Мы можем вызвать метод таким образом, потому что, по соглашению, имя действия совпадает с именем класса, которое реализует действие. JIRA.

  • isUsingConfigBean возвращает false, что указывает на то, что этот импортер не позволит клиентам сохранять конфигурацию процесса импорта.

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

Шаг 6. Создайте класс SimpleCsvConfigBean.

Теперь создайте класс SimpleCsvConfigBean, com.example.plugins.tutorial.jira.csvimport.SimpleCsvConfigBean со следующим кодом:


package com.example.plugins.tutorial.jira.csvimport;

import com.atlassian.jira.plugins.importer.external.beans.ExternalCustomField;
import com.atlassian.jira.plugins.importer.imports.config.ValueMappingDefinition;
import com.atlassian.jira.plugins.importer.imports.config.ValueMappingDefinitionsFactory;
import com.atlassian.jira.plugins.importer.imports.config.ValueMappingHelper;
import com.atlassian.jira.plugins.importer.imports.config.ValueMappingHelperImpl;
import com.atlassian.jira.plugins.importer.imports.importer.AbstractConfigBean2;
import com.example.plugins.tutorial.jira.csvimport.mapping.PriorityValueMappingDefinition;
import com.google.common.collect.Lists;
import java.util.List;

public class SimpleCsvConfigBean extends AbstractConfigBean2 {
    private SimpleCsvClient csvClient;
    public SimpleCsvConfigBean(SimpleCsvClient csvClient) {
        this.csvClient = csvClient;
    }
    @Override
    public List<String> getExternalProjectNames() {
        return Lists.newArrayList("project");
    }
    @Override
    public List<ExternalCustomField> getCustomFields() {
        return Lists.newArrayList(ExternalCustomField.createText("cf", "custom field"));
    }
    @Override
    public List<String> getLinkNamesFromDb() {
        return Lists.newArrayList("link");
    }
    @Override
    public ValueMappingHelper initializeValueMappingHelper() {
        final ValueMappingDefinitionsFactory mappingDefinitionFactory = new ValueMappingDefinitionsFactory() {
            public List<ValueMappingDefinition> createMappingDefinitions(ValueMappingHelper valueMappingHelper) {
                final List<ValueMappingDefinition> mappings = Lists.newArrayList();
                mappings.add(new PriorityValueMappingDefinition(getCsvClient(), getConstantsManager()));
                return mappings;
            }
        };
       return new ValueMappingHelperImpl(getWorkflowSchemeManager(),
                getWorkflowManager(), mappingDefinitionFactory, getConstantsManager());
    }
    public SimpleCsvClient getCsvClient() {
        return csvClient;
    }
}

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

  • getExternalProjectNames возвращает список имен проектов во внешней системе. Эти значения отображаются на странице сопоставлений проектов, где пользователь может выбрать внешний проект для сопоставления проектам в JIRA. Имейте в виду, что для нашего случая внешняя система - это просто файл CSV, который нужно импортировать.
  • getCustomFields возвращает список пользовательских полей, определенных во внешней системе. Вы можете использовать фабричные методы в ExternalCustomField для создания различных типов пользовательских полей. Эти пользовательские поля отображаются на пользовательской странице поля, где пользователи могут выбирать, как они будут импортированы в JIRA.
  • getLinkNamesFromDb возвращает имена ссылок из внешней системы. Страница ссылок импортеров показывает эти ссылки и позволяет пользователям сопоставлять их с типами ссылок JIRA.
  • initializeValueMappingHelper инициализирует значение защищенного поля valueMappingHelper. ValueMappingHelper содержит ValueMappingDefinitionsFactory, который отвечает за создание определений сопоставления. Определения сопоставления связывают значения из внешней системы с значениями полей задач в JIRA. В большинстве случаев импортеру необходимо будет учитывать сопоставления для значений статуса, задачи, типа, разрешения и приоритета. Но вы можете сопоставить любое значение из задачи, как перечислено в com.atlassian.jira.issue.IssueFieldConstants. Затем мы создадим класс PriorityValueMappingDefinition.

Шаг 7. Создайте класс PriorityValueMappingDefinition

Теперь создайте класс, ответственный за сопоставление значений приоритета из исходной системы в JIRA. Он говорит импортеру, какое внешнее системное поле сопоставлено, с каким полем задачи. Это также определяет доступные значения в JIRA и во внешней системе для поля. Кроме того это может поставлять сопоставление значения по умолчанию между JIRA и внешней системой.

Создайте класс com.example.plugins.tutorial.jira.csvimport.mapping.PriorityValueMappingDefinition со следующим кодом:


package com.example.plugins.tutorial.jira.csvimport.mapping;

import com.atlassian.jira.config.ConstantsManager;
import com.atlassian.jira.issue.IssueConstant;
import com.atlassian.jira.issue.IssueFieldConstants;
import com.atlassian.jira.plugins.importer.imports.config.ValueMappingDefinition;
import com.atlassian.jira.plugins.importer.imports.config.ValueMappingEntry;
import com.example.plugins.tutorial.jira.csvimport.Issue;
import com.example.plugins.tutorial.jira.csvimport.SimpleCsvClient;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Set;

public class PriorityValueMappingDefinition implements ValueMappingDefinition {
    private final SimpleCsvClient simpleCsvClient;
    private final ConstantsManager constantsManager;
    public PriorityValueMappingDefinition(SimpleCsvClient simpleCsvClient, ConstantsManager constantsManager) {
        this.simpleCsvClient = simpleCsvClient;
        this.constantsManager = constantsManager;
    }
    @Override
    public String getJiraFieldId() {
        return IssueFieldConstants.PRIORITY;
    }
    @Override
    public Collection<ValueMappingEntry> getTargetValues() {
        return new ArrayList<ValueMappingEntry>(Collections2.transform(constantsManager.getPriorityObjects(),
                new Function<IssueConstant, ValueMappingEntry>() {
                    public ValueMappingEntry apply(IssueConstant from) {
                        return new ValueMappingEntry(from.getName(), from.getId());
                    }
                }));
    }
    @Override
    public boolean canBeBlank() {
        return false;
    }
    @Override
    public boolean canBeCustom() {
        return true;
    }
    @Override
    public boolean canBeImportedAsIs() {
        return true;
    }
    @Override
    public String getExternalFieldId() {
        return "priority";
    }
    @Override
    public String getDescription() {
        return null;
    }
    @Override
    public Set<String> getDistinctValues() {
        return Sets.newHashSet(Iterables.transform(simpleCsvClient.getInternalIssues(),
                new Function<Issue, String>() {
                    @Override
                    public String apply(Issue from) {
                        return from.getPriority();
                    }
                }));
    }
    @Override
    public Collection<ValueMappingEntry> getDefaultValues() {
        return new ImmutableList.Builder<ValueMappingEntry>().add(
                new ValueMappingEntry("Low", IssueFieldConstants.TRIVIAL_PRIORITY_ID),
                new ValueMappingEntry("Normal", IssueFieldConstants.MINOR_PRIORITY_ID),
                new ValueMappingEntry("High", IssueFieldConstants.MAJOR_PRIORITY_ID),
                new ValueMappingEntry("Urgent", IssueFieldConstants.CRITICAL_PRIORITY_ID),
                new ValueMappingEntry("Immediate", IssueFieldConstants.BLOCKER_PRIORITY_ID)
        ).build();
    }
    @Override
    public boolean isMandatory() {
        return false;
    }
}

Класс реализует эти методы:

  • getJiraFieldId возвращает значения из IssueFieldConstants, который определяет, к каким JIRA-полям относится это сопоставление.
  • getTargetValues возвращает значения из JIRA для этого пользовательского поля. Мы используем constantsManager для получения этих значений.
  • canBeBlank, если true, это поле является необязательным в пользовательском интерфейсе.
  • canBeCustom, если true, пользователь может создавать новые значения на странице сопоставления значений импортера. Используйте этот параметр только в том случае, если значения для этого поля JIRA могут быть созданы динамически.
  • canBeImportedAsIs возвращает true, если пользователю не нужно указывать значение для этого поля. В этом случае исходное значение может быть импортировано из исходной системы «как есть» (то есть без дальнейшей обработки).
  • getExternalFieldId - это имя этого поля во внешней системе.
  • getDescription - это необязательное описание, отображаемое пользователю на странице сопоставлений значений импортера.
  • getDistinctValues возвращает только отдельные значения из внешней системы.
  • getDefaultValues возвращает сопоставления по умолчанию между значениями во внешней системе и JIRA.
  • isMandatory должен возвращать true, если пользователю необходимо отобразить это поле.

Шаг 8. Создайте класс SimpleCsvDataBean.

Класс компонента данных обрабатывает задачу преобразования импортированных данных.

Создайте класс com.example.plugins.tutorial.jira.csvimport.SimpleCsvDataBean со следующим кодом:


package com.example.plugins.tutorial.jira.csvimport;

import com.atlassian.jira.issue.IssueFieldConstants;
import com.atlassian.jira.plugins.importer.external.CustomFieldConstants;
import com.atlassian.jira.plugins.importer.external.beans.*;
import com.atlassian.jira.plugins.importer.imports.config.ValueMappingHelper;
import com.atlassian.jira.plugins.importer.imports.importer.AbstractDataBean;
import com.atlassian.jira.plugins.importer.imports.importer.ImportLogger;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.apache.commons.lang.StringUtils;
import java.util.*;

public class SimpleCsvDataBean extends AbstractDataBean<SimpleCsvConfigBean> {
    private final SimpleCsvClient csvClient;
    private final SimpleCsvConfigBean configBean;
    private final ValueMappingHelper valueMappingHelper;
    public SimpleCsvDataBean(SimpleCsvConfigBean configBean) {
        super(configBean);
        this.configBean = configBean;
        this.csvClient = configBean.getCsvClient();
        this.valueMappingHelper = configBean.getValueMappingHelper();
    }
    @Override
    public Set<ExternalUser> getRequiredUsers(Collection<ExternalProject> projects, ImportLogger importLogger) {
        return getAllUsers(importLogger);
    }
    @Override
    public Set<ExternalUser> getAllUsers(ImportLogger log) {
        return Sets.newHashSet(Iterables.transform(csvClient.getInternalIssues(), new Function<Issue, ExternalUser>() {
            @Override
            public ExternalUser apply(Issue from) {
                return new ExternalUser(from.getAssignee(), from.getAssignee());
            }
        }));
    }
    @Override
    public Set<ExternalProject> getAllProjects(ImportLogger log) {
        final ExternalProject project = new ExternalProject(
                configBean.getProjectName("project"),
                configBean.getProjectKey("project"));
        project.setExternalName("project");
        return Sets.newHashSet(project);
    }
    @Override
    public Iterator<ExternalIssue> getIssuesIterator(ExternalProject externalProject, ImportLogger importLogger) {
        return Iterables.transform(csvClient.getInternalIssues(), new Function<Issue, ExternalIssue>() {
            @Override
            public ExternalIssue apply(Issue from) {
                final ExternalIssue externalIssue = new ExternalIssue();
                final String priorityMappedValue = valueMappingHelper.getValueMapping("priority", from.getPriority());
                externalIssue.setPriority(StringUtils.isBlank(priorityMappedValue) ? from.getPriority() : priorityMappedValue);
                externalIssue.setExternalId(from.getIssueId());
                externalIssue.setSummary(from.getSummary());
                externalIssue.setAssignee(from.getAssignee());
                externalIssue.setExternalCustomFieldValues(Lists.newArrayList(
                        new ExternalCustomFieldValue(configBean.getFieldMapping("cf"), CustomFieldConstants.TEXT_FIELD_TYPE,
                                CustomFieldConstants.TEXT_FIELD_SEARCHER, from.getCustomField())));
                externalIssue.setStatus(String.valueOf(IssueFieldConstants.OPEN_STATUS_ID));
                externalIssue.setIssueType(IssueFieldConstants.BUG_TYPE);
                return externalIssue;
            }
        }).iterator();
    }
    @Override
    public Collection<ExternalLink> getLinks(ImportLogger log) {
        final List<ExternalLink> externalLinks = Lists.newArrayList();
        final String linkName = configBean.getLinkMapping("link");
        for (Issue issue : csvClient.getInternalIssues()) {
            if (StringUtils.isNotBlank(issue.getLinkedIssueId())) {
                externalLinks.add(new ExternalLink(linkName, issue.getIssueId(), issue.getLinkedIssueId()));
            }
        }
        return externalLinks;
    }
    @Override
    public long getTotalIssues(Set<ExternalProject> selectedProjects, ImportLogger log) {
        return csvClient.getInternalIssues().size();
    }
    @Override
    public String getUnusedUsersGroup() {
        return "simple_csv_import_unused";
    }
    @Override
    public void cleanUp() {
    }
    @Override
    public String getIssueKeyRegex() {
        return null;
    }
    @Override
    public Collection<ExternalVersion> getVersions(ExternalProject externalProject, ImportLogger importLogger) {
        return Collections.emptyList();
    }
    @Override
    public Collection<ExternalComponent> getComponents(ExternalProject externalProject, ImportLogger importLogger) {
        return Collections.emptyList();
    }
}

Класс extends AbstractDataBean <SimpleCsvConfigBean>, вспомогательный класс, который объединяет наш конфигурационный компонент и позволяет использовать пользователю  сопоставлямые значения для проектов и задач. Мы должны внедрить эти методы, чтобы заставить наш компонент данных работать:

  • getRequiredUsers возвращает учетные записи пользователей, которые эффективно используются в проекте JIRA, предназначенном для импорта. В нашем случае мы просто возвращаем всех пользователей, поскольку мы используем их всех.
  • getAllUsers возвращает пользователей из внешней системы.
  • getAllProjects возвращает все проекты из внешней системы. В нашем случае мы возвращаем один проект. Обратите внимание, что мы используем configBean.getProjectName («project») и configBean.getProjectKey («project») для получения значений, определенных пользователем для проекта с именем «project», как указано на странице «Сопоставление проектов импортеров».
  • getIssuesIterator возвращает итератор со всеми задачами во внешнем проекте. В этом методе импортер отвечает за сопоставление пользовательских полей и значений с выбранных пользователем.Обратите внимание на следующие моменты:
    • Мы используем valueMappingHelper.getValueMapping («priority», from.getPriority ()), чтобы получить значение приоритета, выбранное пользователем. Строка «priority» - это имя поля внешней системы, определенного в нашем классе PriorityValueMappingDefinition. Поскольку мы решили, что пользователь может использовать каждое значение «как есть», мы должны проверить, предоставил ли пользователь значение для этого сопоставляемого значения. Если нет, мы должны использовать исходное значение из внешней системы.
    • Мы используем configBean.getFieldMapping («cf»), чтобы получить выбранное пользователем имя для пользоваетльского поля. Мы используем один и тот же идентификатор «cf», который был определен в SimpleCsvConfigBean # getCustomFields.
  • getLinks возвращает все ссылки между задачами во внешней системе. Вызов configBean.getLinkMapping («link») получает имя ссылки, которую пользователь выбрал, передал как параметр «ссылка» и который определен в SimpleCsvConfigBean # getLinkNamesFromDb.
  • getTotalIssues возвращает все задачи для выбранных проектов.
  • getUnusedUsersGroup возвращает группу JIRA, которая будет создана для неактивных пользователей.
  • cleanUp вызывается после завершения импорта. Здесь вы можете очистить использованные ресурсы.
  • getIssueKeyRegex может возвращать регулярное выражение, которое соответствует ключам задач внешней системы. Согласованные выражения в резюме, комментариях и полях описания заменяются назначенным ключом задачи JIRA. Например, («случай: 2842») будет переписан на ссылки JIRA («JRA-2848»).
  • getVersions возвращает все версии для данного проекта. В нашем примере мы не обрабатываем версии, поэтому просто возвращаем пустую коллекцию.
  • getComponents возвращает все компоненты для данного проекта. В нашем примере мы не обрабатываем компоненты, поэтому просто возвращаем пустую коллекцию.

Шаг 9. Определите текстовое поле файла CSV как действие веб-сайта

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

Наша страница настройки состоит из нескольких частей: класса, который реализует страницу настройки, шаблон Velocity и модуль дескриптора, который связывает их вместе. Начнем с класса:

  1. Создайте класс com.example.plugins.tutorial.jira.csvimport.web.SimpleCsvSetupPage со следующим содержимым

package com.example.plugins.tutorial.jira.csvimport.web;

import com.atlassian.jira.plugins.importer.extensions.ImporterController;
import com.atlassian.jira.plugins.importer.tracking.UsageTrackingService;
import com.atlassian.jira.plugins.importer.web.AbstractSetupPage;
import com.atlassian.jira.plugins.importer.web.ConfigFileHandler;
import com.atlassian.jira.security.xsrf.RequiresXsrfCheck;
import com.atlassian.plugin.PluginAccessor;
import com.atlassian.plugin.web.WebInterfaceManager;
import com.example.plugins.tutorial.jira.csvimport.SimpleCsvClient;

public class SimpleCsvSetupPage extends AbstractSetupPage {
    private String filePath;
    public SimpleCsvSetupPage(UsageTrackingService usageTrackingService, WebInterfaceManager webInterfaceManager, PluginAccessor pluginAccessor) {
        super(usageTrackingService, webInterfaceManager, pluginAccessor);
    }
    @Override
    public String doDefault() throws Exception {
        if (!isAdministrator()) {
            return "denied";
        }
        final ImporterController controller = getController();
        if (controller == null) {
            return RESTART_NEEDED;
        }
        return INPUT;
    }
    @Override
    @RequiresXsrfCheck
    protected String doExecute() throws Exception {
        final ImporterController controller = getController();
        if (controller == null) {
            return RESTART_NEEDED;
        }
        if (!isPreviousClicked() && !controller.createImportProcessBean(this)) {
            return INPUT;
        }
        return super.doExecute();
    }
    @Override
    protected void doValidation() {
        if (isPreviousClicked()) {
            return;
        }
        try {
            new SimpleCsvClient(filePath);
        } catch (RuntimeException e) {
            addError("filePath", e.getMessage());
        }
        super.doValidation();
    }
    public String getFilePath() {
        return filePath;
    }
    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }
}

                Обратите внимание на несколько вещей:

  • Аннотации @RequiresXsrfCheck для метода doExecute важны! Если этого не происходит, пользователю не нужно указывать учетные данные администратора для изменения состояния JIRA, что создало бы серьезную дыру безопасности в экземпляре JIRA. Не забудьте добавить его в собственные реализации импортера.
  • Чтобы проверить ввод, doValidation () просто пытается создать экземпляр SimpleCsvClient, чтобы узнать, действителен ли предоставленный путь к файлу.
  1. Вернитесь в файл дескриптора, atlassian-plugin.xml, добавьте следующий модуль действия веб-сайта:

<webwork1 key="actions" name="Actions">
    <actions>
        <action name="com.example.plugins.tutorial.jira.csvimport.web.SimpleCsvSetupPage" alias="SimpleCsvSetupPage">
            <view name="input">templates/csvSetupPage.vm</view>
            <view name="denied">/secure/views/securitybreach.jsp</view>
            <view name="restartimporterneeded">/templates/admin/views/restartneeded.vm</view>
        </action>
    </actions>
</webwork1>

Модуль ссылается на только что добавленный вами класс установочной страницы и шаблон Velocity, который отобразит нашу форму. Для атрибута атрибута alias важно, чтобы имя класса было реализовано. Также обратите внимание, что мы повторно используем ресурсы страницы из JIRA:

  • restartneeded.vm, из плагина JIM, отображает информацию, указывающую пользователю, что процесс импорта необходимо перезапустить. Это появляется, если пользователь пытается пропустить шаги в мастере настройки или по какой-либо причине завершается сеанс HTTP.
  • securitybreach.jsp, из ядра JIRA, отображает информацию о несанкционированном использовании.
  1. Создайте шаблон Velocity для нашей страницы настройки, добавив следующий код в файл csvSetupPage.vm в новый каталог src / main / resources / templates:

#parse('/templates/admin/views/common/import-header.vm')
#set ($auiparams = 
    $map.build('name', 'filePath', 'label', "CSV File path", 'size', 60, 'value', $action.filePath, 'required', true))
#parse("/templates/standard/textfield.vm")

#parse("/templates/admin/views/common/standardSetupFooter.vm")

 Это страница, которая позволяет пользователю вводить путь к CSV-файлу, который они хотят импортировать. В нем мы используем шаблоны, предоставленные JIM, для форматирования нашей страницы:

 

  • import-header.vm добавляет стандартный заголовок для всех импортеров JIRA.
  • textfield.vm отображает простой ввод HTML с типом текста и параметрами, представленными в переменной auiparams.
  • standardSetupFooter.vm ставит кнопки Next и Back на странице вместе со стандартным нижним колонтитулом.

Шаг 10. Создайте класс и модель данных SimpleCsvClient.

Наконец, давайте создадим класс, который имитирует наш внешний трекер  задач и моделирует данные в нашей  внешней системе отслеживания задач. Эти данные будут в файлах CSV и состоят из шести полей, разделенных запятой. Поля являются issueId, summary, priority, assignee, customField и inkedIssueId.

Создайте com.example.plugins.tutorial.jira.csvimport.SimpleCsvClient, чтобы имитировать наш внешний трекер задач:

Создайте com.example.plugins.tutorial.jira.csvimport.SimpleCsvClient, чтобы имитировать наш внешний трекер:

SimpleCsvClient


 package com.example.plugins.tutorial.jira.csvimport;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import org.apache.commons.io.IOUtils;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
 * we assume data is in constant format
 * issueId,summary, priority, assignee, customField,linkedIssueId
 */
public class SimpleCsvClient {
    private List<Issue> internalIssues;
    public List<Issue> getInternalIssues() {
        return internalIssues;
    }
    public SimpleCsvClient(String filePath) {
        try {
            final FileInputStream input = new FileInputStream(filePath);
            try {
                final List<String> content = IOUtils.readLines(input);
                internalIssues = Lists.newArrayListWithExpectedSize(content.size());
                final Splitter splitter = Splitter.on(",").trimResults();
                for (int i = 0, contentSize = content.size(); i < contentSize; i++) {
                    final String issueLine = content.get(i);
                    final ArrayList<String> values = Lists.newArrayList(splitter.split(issueLine));
                    if (values.size() != 6) {
                        throw new RuntimeException("Invalid line " + (i + 1) + " " + issueLine + " should contain six values");
                    }
                    internalIssues.add(new Issue(values));
                }
            } finally {
                input.close();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

Здесь не большой интерес. Основной момент, который нужно знать, состоит в том, что SimpleCsvClient способен возвращать список задач, содержащих шесть значений: issueId, summary, priority, assignee, customField, linkedIssueId.

Теперь создайте класс задачи com.example.plugins.tutorial.jira.csvimport.Issue:


package com.example.plugins.tutorial.jira.csvimport;
import java.util.List;
/**
 * Helper class to convert issues from csv values
 */
public class Issue {
    private final String issueId;
    private final String summary;
    private final String priority;
    private final String assignee;
    private final String customField;
    private final String linkedIssueId;
    public Issue(List values) {
        issueId = values.get(0);
        summary = values.get(1);
        priority = values.get(2);
        assignee = values.get(3);
        customField = values.get(4);
        linkedIssueId = values.get(5);
    }
    public String getIssueId() {
        return issueId;
    }
    public String getSummary() {
        return summary;
    }
    public String getPriority() {
        return priority;
    }
    public String getAssignee() {
        return assignee;
    }
    public String getCustomField() {
        return customField;
    }
    public String getLinkedIssueId() {
        return linkedIssueId;
    }
}

Создайте данные для нашей внешней системы. Добавьте следующие данные в файл с именем test.csv в папке test / resources:


1,summary 1,Low,user1,value1,2
2,summary 2,High,user1,value2,3
3,summary 3,Low,user2,value1,
4,summary 4,Unknown,user3,value3,

Тот же файл проверяется в репозитории учебников на Bitbucket. Имя файла и его местоположение (test / resources / test.csv) сейчас не так важны; пользователи импортера могут указать файл в любом месте страницы настройки. Но там, где интеграционный тест, который мы будем строить позже, ожидает, что будут тестовые данные, поэтому его размещение теперь сохраняет шаг на потом.

Мы готовы запустить JIRA и посмотреть, что у нас есть.

Шаг 11. Запустите JIRA и попробуйте импортер

Выполните следующие шаги, чтобы установить и протестировать плагин импортера:

  1. В окне терминала перейдите в корневую папку плагина (где находится файл pom.xml) и запустите atlas-run (или atlas-debug, если вы хотите запустить отладчик в своей среде IDE).

JIRA берет несколько минут, чтобы загрузить и запустить.

  1. Когда JIRA завершит запуск, откройте домашнюю страницу JIRA в браузере. JIRA печатает URL-адрес, который будет использоваться на выходе консоли.
  2. Войдите в систему с комбинацией пользователя и пароля по умолчанию, admin / admin.
  3. Создайте новый проект на основе шаблона разработки программного обеспечения. Вам понадобится это, потому что в этом вопросе говорится, что наш плагин зависит (например, от открытого состояния).
  4. В заголовке JIRA выберите «Проекты»> «Импорт внешнего проекта».
  5. Найдите своего импортера в списке. Как вы можете понять, это он назван Simple CSV Importer:

РИСУНОК

  1. Нажмите на импортер, чтобы запустить мастер. Вы должны увидеть первый шаг мастера, созданную страницу настройки:

РИСУНОК

  1. В поле «Путь к файлу CSV» введите путь к файлу и нажмите «Далее». Например: //home/atlas/atlassian/tutorial-jira-simple-csv-importer/src/test/resources/test.csv
  2. Продолжайте работать с мастером. На шаге 2 обязательно сопоставьте импортированные задачи с созданным проектом разработки программного обеспечения.
  3. Наконец, нажмите кнопку «Начать импорт», чтобы завершить импорт.

Когда это будет сделано, вы получите сообщение, подобное этому: 0 проектов и 4 задачи успешно импортированы! Если вы перейдете к проекту, вы увидите четыре новых задачи в своем проекте.

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

На этом этапе вы можете запустить JIRA и использовать команды FastDev или atlas-cli и pi для переустановки плагина без перезагрузки JIRA.

Шаг 12. Разверните плагин для поддержки файлов конфигурации

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

  1. Добавьте импорт компонента в atlassian-plugin.xml. Это импортирует ConfigFileHandler, который поможет нам справиться с сериализацией и опреснением конфигурации импортера.

 <component-import key="configFileHandler"
                      interface="com.atlassian.jira.plugins.importer.web.ConfigFileHandler"/>

  1. Расширьте класс SimpleCsvSetupPage следующим образом:

 

  1. Добавьте параметр конструктора и поле для ConfigFileHandler:

...
private final ConfigFileHandler configFileHandler;
public SimpleCsvSetupPage(UsageTrackingService usageTrackingService, WebInterfaceManager webInterfaceManager, 
                            PluginAccessor pluginAccessor, ConfigFileHandler configFileHandler) {
    super(usageTrackingService, webInterfaceManager, pluginAccessor);
    this.configFileHandler = configFileHandler;
}
...

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

 

  1. Добавьте этот код проверки как последнюю строку метода doValidation для SimpleCsvSetupPage:

...
protected void doValidation() {
...
    configFileHandler.verifyConfigFileParam(this);
}

 Разверните следующий блок кода, чтобы посмотреть, как выглядит весь класс:


package com.example.plugins.tutorial.jira.csvimport.web;
import com.atlassian.jira.plugins.importer.extensions.ImporterController;
import com.atlassian.jira.plugins.importer.tracking.UsageTrackingService;
import com.atlassian.jira.plugins.importer.web.AbstractSetupPage;
import com.atlassian.jira.plugins.importer.web.ConfigFileHandler;
import com.atlassian.jira.security.xsrf.RequiresXsrfCheck;
import com.atlassian.plugin.PluginAccessor;
import com.atlassian.plugin.web.WebInterfaceManager;
import com.example.plugins.tutorial.jira.csvimport.SimpleCsvClient;
public class SimpleCsvSetupPage extends AbstractSetupPage {
    private String filePath;
    private final ConfigFileHandler configFileHandler;
    public SimpleCsvSetupPage(UsageTrackingService usageTrackingService, WebInterfaceManager webInterfaceManager, PluginAccessor pluginAccessor, ConfigFileHandler configFileHandler) {
        super(usageTrackingService, webInterfaceManager, pluginAccessor);
        this.configFileHandler = configFileHandler;
    }
    @Override
    public String doDefault() throws Exception {
        if (!isAdministrator()) {
            return "denied";
        }
        final ImporterController controller = getController();
        if (controller == null) {
            return RESTART_NEEDED;
        }
        return INPUT;
    }
    @Override
    @RequiresXsrfCheck
    protected String doExecute() throws Exception {
        final ImporterController controller = getController();
        if (controller == null) {
            return RESTART_NEEDED;
        }
        if (!isPreviousClicked() && !controller.createImportProcessBean(this)) {
            return INPUT;
        }
        return super.doExecute();
    }
    @Override
    protected void doValidation() {
        if (isPreviousClicked()) {
            return;
        }
        try {
            new SimpleCsvClient(filePath);
        } catch (RuntimeException e) {
            addError("filePath", e.getMessage());
        }
        super.doValidation();
        configFileHandler.verifyConfigFileParam(this);
    }
    public String getFilePath() {
        return filePath;
    }
    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }
}

  1. Теперь расширяем класс SimpleCsvImporterController следующим образом:
  1. Сначала добавьте параметр и поле конструктора для ConfigFileHandler.

...
private final ConfigFileHandler configFileHandler;
public SimpleCsvImporterController(JiraDataImporter importer, ConfigFileHandler configFileHandler) {
    super(importer, IMPORT_CONFIG_BEAN, IMPORT_ID);
    this.configFileHandler = configFileHandler;
}
...

  1. Заполните конфигурационный файл в методе createImportProcessBean для SimpleCsvImporterController, как показано ниже:

public boolean createImportProcessBean(AbstractSetupPage abstractSetupPage) {
   ...
   final SimpleCsvConfigBean configBean = new SimpleCsvConfigBean(new SimpleCsvClient(setupPage.getFilePath()));
        final ImportProcessBean importProcessBean = new ImportProcessBean();
        if (!configFileHandler.populateFromConfigFile(setupPage, configBean)) {
            return false;
        }
        importProcessBean.setConfigBean(configBean);
...
}

  1. Наконец, удалите метод isUsingConfiguration. По умолчанию (то есть, если он не переопределяется), этот метод возвращает true, что мы и хотим сделать, поддерживая сохранение конфигурации.
  1. Расширьте страницу настроек Velocity, csvSetupPage.vm, чтобы предоставить пользователям файл конфигурации для настроек импорта. Для этого просто добавьте эту директиву синтаксиса в файл csvSetupPage.vm перед директивой standardSetupFooter.vm:

#parse('/templates/admin/views/common/configFile.vm')

Шаг 13. Повторите попытку

Перезагрузите плагин в JIRA и снова попробуйте свой пользовательский импортер. Теперь вы должны увидеть новую опцию на первой странице мастера, флажок "Использовать существующий файл конфигурации":

РИСУНОК

У нас еще нет файла конфигурации, поэтому пройдите  через мастер вручную, как и раньше.

И после завершения процесса импорта вы должны увидеть что-то вроде этого:

РИСУНОК

Обратите внимание на новое предложение в «Что сейчас?». Информация. Сохранение ссылки конфигурации позволяет сохранить конфигурацию в виде файла и повторно использовать ее для выполнения следующего импорта. Даем ей шанс!

Шаг 14. Создание интеграционных тестов

Чтобы завершить изображение, добавим пару интеграционных тестов для нашего плагина. Тестирование единиц измерения тестов выходит за рамки данного руководства. Вместо этого мы опишем, как написать тест интеграции, который использует веб-драйвер и объекты страницы JIRA. Вы узнаете, как описать свои собственные объекты страницы и как их использовать для имитации поведения пользователей для тестирования простого импортера CSV.

Вы заметите, что SDK создал для нас некоторый тестовый код. Эти тесты предназначены для того, чтобы служить отправной точкой для испытаний. Мы будем использовать каталог test, который он создал для следующих дополнительных тестов.

  1. Обновите свой pom.xml с помощью некоторых зависимостей, необходимых для тестов:

<dependency>
    <groupId>com.atlassian.jira</groupId>
    <artifactId>atlassian-jira-pageobjects</artifactId>
    <version>${jira.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.atlassian.jira.plugins</groupId>
    <artifactId>jira-importers-plugin</artifactId>
    <version>${jim.version}</version>
    <classifier>tests</classifier>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.atlassian.jira.tests</groupId>
    <artifactId>jira-testkit-client</artifactId>
    <version>${testkit.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.codehaus.jackson</groupId>
    <artifactId>jackson-core-asl</artifactId>
    <version>1.9.1</version>
</dependency>
<dependency>
    <groupId>org.codehaus.jackson</groupId>
    <artifactId>jackson-xc</artifactId>
    <version>1.9.1</version>
</dependency>
<dependency>
    <groupId>org.codehaus.jackson</groupId>
    <artifactId>jackson-jaxrs</artifactId>
    <version>1.9.1</version>
</dependency>
<dependency>
   <groupId>org.codehaus.jackson</groupId>
   <artifactId>jackson-mapper-asl</artifactId>
   <version>1.9.1</version>
</dependency>

  1. Также в POM добавьте свойство plugins для включения плагина testkit JIRA:

<properties>
    ...
    <testkit.version>5.0-m13</testkit.version>
    <plugins>com.atlassian.jira.plugins:jira-importers-plugin:${jim.version},com.atlassian.jira.tests:jira-testkit-plugin:${testkit.version}</plugins>
    </properties>

Это означает, что при запуске SDK JIRA будет включать в себя плагин testkit JIRA, позволяющий нам изменять состояние JIRA с помощью брандмауэр-плагина.

 

  1. Создайте объект страницы, представляющий SImpleCsvSetupPage. В тестовом каталоге (src / test /) создайте класс com.example.plugins.jira.csvimport.po.SimpleCsvSetupPage со следующим содержимым:

package com.example.plugins.jira.csvimport.po;
 
import com.atlassian.jira.plugins.importer.po.common.AbstractImporterWizardPage;
import com.atlassian.jira.plugins.importer.po.common.ImporterProjectsMappingsPage;
import org.junit.Assert;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;

 
public class SimpleCsvSetupPage extends AbstractImporterWizardPage {
        
    @FindBy(id = "filePath")
    WebElement filePath;
        
    public SimpleCsvSetupPage setFilePath(String filePath) {
        this.filePath.sendKeys(filePath);
        return this;
    }

    @Override
    public String getUrl() {
        return "/secure/admin/views/SimpleCsvSetupPage!default.jspa?externalSystem=" +
                "com.example.plugins.tutorial.jira.simple-csv-importer:SimpleCSVImporterKey";
    }
    public ImporterProjectsMappingsPage next() {
        Assert.assertTrue(nextButton.isEnabled());
        nextButton.click();
        return pageBinder.bind(ImporterProjectsMappingsPage.class);
    }
}

  1. Еще в каталоге тестов создайте тестовый класс it.com.example.plugins.jira.csvimport.TestSimpleCsvImporter.

package it.com.example.plugins.jira.csvimport;
 
import com.atlassian.jira.pageobjects.JiraTestedProduct;
import com.atlassian.jira.pageobjects.config.EnvironmentBasedProductInstance;
import com.atlassian.jira.plugins.importer.po.ExternalImportPage;
import com.atlassian.jira.plugins.importer.po.common.ImporterLinksPage;
import com.atlassian.jira.plugins.importer.po.common.ImporterProjectsMappingsPage;
import com.atlassian.jira.testkit.client.Backdoor;
import com.atlassian.jira.testkit.client.restclient.SearchRequest;
import com.atlassian.jira.testkit.client.restclient.SearchResult;
import com.atlassian.jira.testkit.client.util.TestKitLocalEnvironmentData;
import com.example.plugins.jira.csvimport.po.SimpleCsvSetupPage;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.junit.matchers.JUnitMatchers.hasItem;

public class TestSimpleCsvImporter {

    private JiraTestedProduct jira;
    private Backdoor backdoor;
        
    @Before
    public void setUp() {
        backdoor = new Backdoor(new TestKitLocalEnvironmentData());
        backdoor.restoreBlankInstance();
        jira = new JiraTestedProduct(null, new EnvironmentBasedProductInstance());
    }

        
    @Test
    public void testSimpleCsvImporterAttachedToConfigPage() {
        assertThat(jira.gotoLoginPage().loginAsSysAdmin(ExternalImportPage.class).getImportersOrder(),
                hasItem("SimpleCSVImporter"));
    }

        
    @Test
    public void testSimpleCsvImporterWizard() {
        backdoor.issueLinking().enable();
        backdoor.issueLinking().createIssueLinkType("Related", "related", "related to");
        final SimpleCsvSetupPage setupPage = jira.gotoLoginPage().loginAsSysAdmin(SimpleCsvSetupPage.class);
        final String file = getClass().getResource("/test.csv").getFile();
        final ImporterProjectsMappingsPage projectsPage = setupPage.setFilePath(file).next();
        projectsPage.createProject("project", "JIRA Project", "PRJ");
        final ImporterLinksPage linksPage = projectsPage.next().next().next().next();
        linksPage.setSelect("link", "Related");
        assertTrue(linksPage.next().waitUntilFinished().isSuccess());
        final SearchResult search = backdoor.search().getSearch(new SearchRequest().jql(""));
        assertEquals((Integer) 4, search.total);
    }
}

Обратите внимание, что имя пакета для этого или любого класса тестирования интеграции должно начинаться с него. Рабочая среда SDK ищет интеграционные тесты. Обратите внимание на наш тестовый класс:

  • Во-первых, мы используем метод setUp для создания бэкдор-клиента, который используется для восстановления JIRA в пустом состоянии.
  • Первый из наших двух тестов проверяет, добавлен ли наш импортёр на страницу внешнего импорта.
  • Второй тест проходит через мастер Simple CSV Importer и использует бэкдор(Бэкдор, backdoor (от англ. back door — «чёрный ход», буквально «задняя дверь») — дефект алгоритма, который намеренно встраивается в него разработчиком и позволяет получить несанкционированный доступ к данным или удалённому управлению операционной системой и компьютером в целом), чтобы убедиться, что были созданы четыре задачи.
  1. Тест зависит от вашего CSV-файла, находящегося в определенном месте. Если у вас еще нет тестовых данных в ожидаемом месте, поместите тестовые данные в test / resources / test.csv:

1,summary 1,Low,user1,value1,2
2,summary 2,High,user1,value2,3
3,summary 3,Low,user2,value1,
4,summary 4,Unknown,user3,value3,

  1. Вам также необходимо создать каталог test / xml для тестирования. Он может быть пустым.
  2. Теперь вы готовы запустить тест! В консоли введите команду atlas-integration-test.

Это запускает JIRA, открывает браузер Firefox и запускает тесты. Для получения подробной информации о результатах проверьте файлы журнала в месте, указанном в выводе командного терминала.

 

По материалам Atlassian JIRA  Server Developer Writing a custom importer using the JIRA importers add-on