Всем привет!
В этой статьи мы поговорим от том, как сделать так, чтобы Ваш плагин мог бы быть расширен с помощью дополнительных модулей в сторонних плагинах.
Зачем нам это нужно?
Давайте возьмем плагин 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 как и ожидалось.
У нас все получилось. Мы смогли расширить наш плагин калькулятор из стороннего плагина.
Финальный код вот здесь.