Расширяем на плагин для Jira Server/Data Center с помощью @ModuleType

Всем привет!

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

Зачем нам это нужно?

Давайте возьмем плагин Configuration Manager. Этот плагин переносит конфигурации Jira с одного экземпляра Jira на другой. При переносе Configuration Manager в том числе переносит и кастомные поля. Кастомные поля из коробки Configuration Manager может перенести легко, но что делать с полями из других плагинов. Например, кастомные поля плагина Insight. Конфигурация этих полей храниться в базе данных Jira, в таблицах, которые определены разработчиками Insight. Поэтому, чтобы сделать перенос конфигурации этих полей, разработчикам Configuration Manager, пришлось бы изучить, как хранятся данные конфигурации и сделать перенос. И так нужно было бы поступить со всеми остальными плагинами, которые определяют свои кастомные поля. Это слишком затратно. Да и не нужно. Insight может изменить хранение конфигурации своих полей, и тогда разработчикам Configuration Manager нужно было как-то об этом узнать, а потом внести изменения. Разработчики Configuration Manager сделали проще. Они сделали так, что миграцию кастомных полей можно расширить в стороннем плагине. Т.е. разработчики Insight могут просто реализовать предоставленный интерфейс в Configuration Manager и поля Insight будут успешно перенесены. Вы можете почитать подробнее вот тут.

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

Мы сделаем плагин Калькулятор.

В нашем плагине будет реализована только одна операция “sum”, но мы сделаем наше приложение так, что разработчики других плагинов смогут добавлять свои операции. Мы также побудем этими разработчиками и создадим плагин, который реализует операцию минус для нашего калькулятора.

Ну что, начнем!

Калькулятор

Исходный код для этой части можно посмотреть вот здесь.

Мы будем получать результат опреаций через рест. Поэтому я создал рест.

src/main/java/ru/matveev/alexey/atlassian/calculator/rest/CalculatorRest.java

@Path("/calculate")
public class CalculatorRest {
    private final CalculatorService calculatorService;

    public CalculatorRest(CalculatorService calculatorService) {
        this.calculatorService = calculatorService;
    }

    @POST
    @AnonymousAllowed
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces({MediaType.APPLICATION_JSON})
    public Response calculate(CalculatorModel body) throws OperationNotFoundException {
        int result = this.calculatorService.calculate(body.getOperation(),
                                                      body.getValue1(),
                                                      body.getValue2());
        return Response.ok(new CalculatorResponseModel(result)).build();
    }

Здесь я добавил POST метод. Мы принимаем CalculatorModel в качестве тела запроса и выдаем CalculatorResponseModel в качестве результата.

Вот CalculatorModel.

src/main/java/ru/matveev/alexey/atlassian/calculator/rest/CalculatorModel.java

@Data
public class CalculatorModel {

    private String operation;
    private int value1;
    private int value2;

}

Таким образом мы можем отправлять тело запроса вот так:

{"operation": "sum", "value1": 1, "value2" : 3}

А вот CalculatorResponseModel.

src/main/java/ru/matveev/alexey/atlassian/calculator/rest/CalculatorResponseModel.java

@XmlRootElement(name = "result")
@XmlAccessorType(XmlAccessType.FIELD)
@Data
public class CalculatorResponseModel {

    @XmlElement(name = "result")
    private int result;

    public CalculatorResponseModel(int result) {
        this.result = result;
    }

}

Это означает, что результат будет вот таким:

{"result": 4}

Теперь посмотрим на наш рест подробнее. Мы получаем результат с помощью CalculatorService:

int result = this.calculatorService.calculate(body.getOperation(),
                                                      body.getValue1(),
                                                      body.getValue2());

Вот реализация CalculatorService.

src/main/java/ru/matveev/alexey/atlassian/calculator/service/CalculatorService.java

@Named
public class CalculatorService {
    private final SumOperation sumOperation;
    public CalculatorService(SumOperation sumOperation) {
        this.sumOperation = sumOperation;
    }

    public int calculate(String operation, int val1, int val2) throws OperationNotFoundException {
        if (operation.equals(sumOperation.getName())) {
            return sumOperation.calculate(val1, val2);
        }
        throw new OperationNotFoundException(String.format("Operation %s not found", operation));
    }
}

Как Вы видите, метод calculate проверяет, равно ли имя операции имени операции, определенной в классе SumOperation, если да, то мы вызываем метод calculate в классе SumOperation. Если нет, то бросаем OperationNotFoundException.

src/main/java/ru/matveev/alexey/atlassian/calculator/exception/OperationNotFoundException.java

public class OperationNotFoundException extends Exception {
    public OperationNotFoundException(String errorMessage) {
        super(errorMessage);
    }
}

А вот наш SumOperation:

src/main/java/ru/matveev/alexey/atlassian/calculator/impl/SumOperation.java

@Named
public class SumOperation implements Operation
{

    @Override
    public String getName() {
        return "sum";
    }

    @Override
    public int calculate(int val1, int val2) {
        return val1 + val2;
    }
}

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

src/main/java/ru/matveev/alexey/atlassian/calculator/api/Operation.java

public interface Operation
{
    String getName();
    int calculate(int val1, int val2);
}

Вот и все. Запустим!

Запускаем

Запустите Jira и перейдите в браузере вот по этой ссылке:

http://localhost:2990/jira/plugins/servlet/restbrowser#/resource/calculator-1-0-calculate

У Вас будет интерфейс для доступа к нашему ресту.

В качестве тела запроса передадим вот такой json:

{"operation": "sum", "value1": 1, "value2" : 3}

И Вы увидите вот такой результат:

Все правильно.

Теперь выполним вот такой запрос:

{"operation": "minus", "value1": 1, "value2" : 3}

И мы увидим OperationNotFoundException:

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

Вносим изменения в калькулятор

Раскомментируем зависимость jira-core в pom.xml.

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

Затем добавить пакет com.atlassian.plugin.osgi.bridge.external в Import-Package в файле pom.xml:

<Import-Package>org.springframework.osgi.*;resolution:="optional", com.atlassian.plugin.osgi.bridge.external, org.eclipse.gemini.blueprint.*;resolution:="optional", *</Import-Package>

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

src/main/java/ru/matveev/alexey/atlassian/calculator/module/OperationModuleDescriptor.java

public class OperationModuleDescriptor extends AbstractModuleDescriptor<Operation>
{
    public OperationModuleDescriptor(final @ComponentImport ModuleFactory moduleFactory)
    {
        super(moduleFactory);
    }

    @Override
    public Operation getModule()
    {
        return moduleFactory.createModule(moduleClassName, this);
    }
}

Как Вы видите, наш дескриптор реализует AbstractModuleDescriptor типа Operation.

Теперь зарегистрируем наш дескриптор.

src/main/java/ru/matveev/alexey/atlassian/calculator/module/BasicModuleTypeFactory.java

@ModuleType(ListableModuleDescriptorFactory.class)
@Named
public class BasicModuleTypeFactory extends
        SingleModuleDescriptorFactory<OperationModuleDescriptor>
{
    @Autowired
    public BasicModuleTypeFactory(HostContainer hostContainer)
    {
        super(hostContainer, "calculatorOperation", OperationModuleDescriptor.class);
    }
}

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

Теперь внесем изменения в CalculatorService.

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

Я поменял код src/main/java/ru/matveev/alexey/atlassian/calculator/service/CalculatorService.java на вот такой:

@Named
public class CalculatorService {
    private final SumOperation sumOperation;
    private final PluginAccessor pluginAccessor;
    public CalculatorService(final @ComponentImport PluginAccessor pluginAccessor,  SumOperation sumOperation) {
        this.sumOperation = sumOperation;
        this.pluginAccessor = pluginAccessor;
    }

    public int calculate(String operation, int val1, int val2) throws OperationNotFoundException {
        if (operation.equals(sumOperation.getName())) {
            return sumOperation.calculate(val1, val2);
        }
        Operation operationModule = this.getModuleForOperationName(operation);
        if (operationModule != null) {
            return operationModule.calculate(val1, val2);
        }
        throw new OperationNotFoundException(String.format("Operation %s not found", operation));
    }

    private Operation getModuleForOperationName(String operationName) {
        List<OperationModuleDescriptor> operationModuleDescriptors =
                pluginAccessor.getEnabledModuleDescriptorsByClass(OperationModuleDescriptor.class);
        for (OperationModuleDescriptor operationModuleDescriptor : operationModuleDescriptors)
        {
            if (operationName.equals(operationModuleDescriptor.getModule().getName())) {
                return operationModuleDescriptor.getModule();
            }
        }
        return null;
    }
}

Я добавил метод getModuleForOperationName, который возвращает ссылку на модуль, если имя операции в этом модуле равно той операции, которую мы пытаемся выполнить.

И я переделал метод calculate:

Operation operationModule = this.getModuleForOperationName(operation);
if (operationModule != null) {
   return operationModule.calculate(val1, val2);
}

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

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

Calculator-extension

Я создал стандартный плагин из from Atlassian SDK.

В файл pom.xml я добавил зависимость на jar нашего плагина calculator. Нам нужен будет интерфейс Operation оттуда.

<dependency>
            <groupId>ru.matveev.alexey.atlassian</groupId>
            <artifactId>calculator</artifactId>
            <version>1.0.0-SNAPSHOT</version>
            <scope>system</scope>
            <systemPath>${project.basedir}/../calculator/target/calculator-1.0.0-SNAPSHOT.jar</systemPath>
</dependency>

И потом я сделал операцию minus.

src/main/java/ru/matveev/alexey/atlassian/operation/MinusOperation.java

public class MinusOperation implements Operation {
    @Override
    public String getName() {
        return "minus";
    }

    @Override
    public int calculate(int val1, int val2) {
        return val1 - val2;
    }
}

Я реализовал все методы интерфейса Operation: getName (возвращает значение “minus”) и calculate.

Теперь я определю модуль calculatorOperation в файле atlassian-plugin.xml.

src/main/resources/atlassian-plugin.xml

 <calculatorOperation key="minus-operation"  class="ru.matveev.alexey.atlassian.operation.MinusOperation"/>

И все. Теперь установим наш новый плагин на тот же экземпляр Jira, где и плагин calculator.

Test extension app

Я установил два плагина на один и тот же экземпляр Jira.

И теперь я выполню наш рест http://localhost:2990/jira/rest/calculator/1.0/calculate вот с таким телом запроса:

{“operation”: “minus”, “value1”: 1, “value2” : 3}

Как Вы видите, я использую операцию minus из плагина calculator-extension.

И мы видим вот такой результат:

Результат -2 как и ожидалось.

У нас все получилось. Мы смогли расширить наш плагин калькулятор из стороннего плагина.

Финальный код вот здесь.

If you have found a spelling error, please, notify us by selecting that text and pressing Ctrl+Enter.

Leave a Reply

%d bloggers like this:

Spelling error report

The following text will be sent to our editors: