On this page:Contents
Prerequisites
For this tutorial, you needEnsure that you are good on the following prerequisites:
- A decent grasp of the the Java language
- Basic knowledge about the toolchain (maven specifically)
- Some Certain Spring framework notions (injection)
- You will need to know some Atlassian add-on Know Atlassian app basics (see https://developer.atlassian.com/ for details). You will also need Atlassian SDK (see https://developer.atlassian.com/docs/getting-started/set-up-the-atlassian-plugin-sdk-and-build-a-project this documentation for reference)
- An IDE (e.g. Eclipse). We use JetBrains IntelliJ Idea here.
...
Introduction
...
Adding functionality into SIL means that you need to create an Atlassian add-on. Add-ons are basically JAR files containing an descriptor (atlassian-plugin.xml). So let's create one, skipping the tools that Atlassian provides, because there's too much to be customized , as you will see soon enough.
So, first thing to do is to create a skeleton maven project; name it how you want and choose a directory where the project will reside. Choose com.mycompany.silexample as a base package, where your sources will reside.
Upon finishing your IDE wizard, you will notice that there's a pom.xml in the root of the project. Let's open this up Open it and replace its content with the following:
Expand |
---|
Code Block |
---|
| <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany</groupId>
<artifactId>silexample</artifactId>
<version>4.0.0</version>
<name>silexample</name>
<description>Example: integrating SIL routines and descriptors</description>
<packaging>atlassian-plugin</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<katlcommons.version>4.0.0-SNAPSHOT</katlcommons.version>
<spring.osgi.version>1.2.1</spring.osgi.version>
<spring.version>4.1.6.RELEASE</spring.version>
<jira.version>7.0.0</jira.version>
<jira.data.version>7.0.0</jira.data.version>
<jira.sal.version>3.0.0</jira.sal.version>
<jira.event.version>2.3.5</jira.event.version>
<atlassian.spring.scanner.version>1.2.13</atlassian.spring.scanner.version>
<plugin.testrunner.version>2.0.0</plugin.testrunner.version>
<amps.version>6.1.2</amps.version>
</properties>
<dependencies>
<dependency>
<groupId>com.atlassian.jira</groupId>
<artifactId>jira-api</artifactId>
<version>${jira.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.sal</groupId>
<artifactId>sal-api</artifactId>
<version>${jira.sal.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.event</groupId>
<artifactId>atlassian-event</artifactId>
<version>${jira.event.version}</version>
<scope>provided</scope>
</dependency>
<!--
This is for the annotations you need to indicate components and OSGI services.
-->
<dependency>
<groupId>com.atlassian.plugin</groupId>
<artifactId>atlassian-spring-scanner-annotation</artifactId>
<version>${atlassian.spring.scanner.version}</version>
<scope>compile</scope>
</dependency>
<!--
This is the runtime code for interpreting the build time index files
-->
<dependency>
<groupId>com.atlassian.plugin</groupId>
<artifactId>atlassian-spring-scanner-runtime</artifactId>
<version>${atlassian.spring.scanner.version}</version>
<scope>runtime</scope>
</dependency>
<!-- ===================================== -->
<!-- Kepler dependencies -->
<!-- ===================================== -->
<dependency>
<groupId>com.keplerrominfo.jira.plugins</groupId>
<artifactId>katl-commons</artifactId>
<scope>provided</scope>
<version>${katlcommons.version}</version>
</dependency>
<!-- ===================================== -->
<!-- Spring & common dependencies -->
<!-- ===================================== -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<scope>provided</scope>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
<scope>provided</scope>
</dependency>
<!-- ===================================== -->
<!-- TEST dependencies -->
<!-- ===================================== -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.6.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.atlassian.jira</groupId>
<artifactId>jira-tests</artifactId>
<version>${jira.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.atlassian.plugins</groupId>
<artifactId>atlassian-plugins-osgi-testrunner</artifactId>
<version>${plugin.testrunner.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.atlassian.jira</groupId>
<artifactId>jira-func-tests</artifactId>
<version>${jira.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>atlassian-plugin.xml</exclude>
<exclude>META-INF/KINFO</exclude>
<exclude>META-INF/KVERSION</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>atlassian-plugin.xml</include>
<include>META-INF/KINFO</include>
<include>META-INF/KVERSION</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>com.atlassian.maven.plugins</groupId>
<artifactId>maven-jira-plugin</artifactId>
<version>${amps.version}</version>
<extensions>true</extensions>
<configuration>
<compressResources>false</compressResources>
<useFastdevCli>false</useFastdevCli>
<enableFastdev>false</enableFastdev>
<enableDevToolbox>false</enableDevToolbox>
<enablePde>false</enablePde>
<enableQuickReload>true</enableQuickReload>
<quickReloadVersion>1.29</quickReloadVersion>
<productVersion>${jira.version}</productVersion>
<productDataVersion>${jira.version}</productDataVersion>
<!-- Enable here the products if you want container tests (you should) -->
<products>
<product>
<id>jira</id>
<instanceId>jira-${jira.version}</instanceId>
<version>${jira.version}</version>
<dataVersion>${jira.data.version}</dataVersion>
<!-- dataPath>${basedir}/src/test/resources/generated-test-resources.zip</dataPath -->
</product>
</products>
<pluginArtifacts>
<!-- installed in the product when using atlas-run or atlas-debug -->
<pluginArtifact>
<groupId>com.keplerrominfo.jira.plugins</groupId>
<artifactId>katl-commons</artifactId>
<version>${katlcommons.version}</version>
</pluginArtifact>
</pluginArtifacts>
<pluginDependencies>
<!-- OBR dependencies -->
<pluginDependency>
<groupId>com.keplerrominfo.jira.plugins</groupId>
<artifactId>katl-commons</artifactId>
</pluginDependency>
</pluginDependencies>
<testGroups>
<testGroup>
<id>wired-jira-integration</id>
<productIds>
<productId>jira-test</productId>
</productIds>
<includes>
<include>it/**/*WiredTest.java</include>
</includes>
</testGroup>
</testGroups>
<testBundleExcludes>
<testBundleExclude>
<groupId>com.keplerrominfo.jira.plugins</groupId>
<artifactId>katl-commons</artifactId>
</testBundleExclude>
<testBundleExclude>
<groupId>com.keplerrominfo</groupId>
<artifactId>sil</artifactId>
</testBundleExclude>
<testBundleExclude>
<groupId>com.keplerrominfo</groupId>
<artifactId>stl</artifactId>
</testBundleExclude>
<testBundleExclude>
<groupId>com.keplerrominfo</groupId>
<artifactId>sil-remote-client</artifactId>
</testBundleExclude>
<testBundleExclude>
<groupId>com.keplerrominfo</groupId>
<artifactId>sil-sms-client</artifactId>
</testBundleExclude>
<testBundleExclude>
<groupId>com.keplerrominfo</groupId>
<artifactId>kepler-util</artifactId>
</testBundleExclude>
<testBundleExclude>
<groupId>com.keplerrominfo</groupId>
<artifactId>kepler-refapp</artifactId>
</testBundleExclude>
<testBundleExclude>
<groupId>xalan</groupId>
<artifactId>xalan</artifactId>
</testBundleExclude>
<testBundleExclude>
<groupId>xml-apis</groupId>
<artifactId>xml-apis</artifactId>
</testBundleExclude>
</testBundleExcludes>
<instructions>
<Bundle-Name>SIL Example</Bundle-Name>
<Bundle-SymbolicName>com.mycompany.silexample</Bundle-SymbolicName>
<Atlassian-Plugin-Key>com.mycompany.silexample</Atlassian-Plugin-Key>
<Spring-Context>*</Spring-Context>
<Export-Package>
!net.java.ao,
!com.atlassian,
com.mycompany.silexample.*
</Export-Package>
<Import-Package>
com.keplerrominfo.refapp.launcher;resolution:="optional",
com.keplerrominfo.refapp.launcher.*;resolution:="optional",
org.springframework.osgi.*;resolution:="optional",
org.eclipse.gemini.blueprint.*;resolution:="optional",
com.atlassian.plugin.osgi.bridge.external,
*;resolution:="optional"
</Import-Package>
</instructions>
<skipManifestValidation>true</skipManifestValidation>
<systemPropertyVariables>
<webdriver.browser>htmlunit</webdriver.browser>
<!--
You can also use chrome if you have it installed.
You'll also need a webDriver for Chrome, which you can get from
https://sites.google.com/a/chromium.org/chromedriver/downloads and place it somewhere on the disk.
Then just comment the htmlunit driver above and uncomment below.
-->
<!--<webdriver.browser>chrome</webdriver.browser>-->
<!--<webdriver.chrome.driver>D:/tools/chromedriver.exe</webdriver.chrome.driver>-->
</systemPropertyVariables>
</configuration>
</plugin>
<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>
<!-- process-classes seems to be skipped if you are using scala
so perhaps use prepare-package -->
<phase>process-classes</phase>
</execution>
</executions>
<configuration>
<includeExclude>+com.mycompany.*</includeExclude>
<verbose>true</verbose>
<scannedDependencies>
<!--
- Add here the scanned dependencies, if any
<dependency>
<groupId>com.somegroup</groupId>
<artifactId>artifact.id</artifactId>
</dependency>
-->
</scannedDependencies>
</configuration>
</plugin>
</plugins>
</build>
<reporting>
<plugins>
<plugin>
<artifactId>maven-javadoc-plugin</artifactId>
<version>LATEST</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.4.3</version>
<configuration>
<argLine>-Xms128m -Xmx256m</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>LATEST</version>
</plugin>
</plugins>
</reporting>
</project> |
|
You will notice that everything revolves around two maven plugins, provided my Atlassian:
...
...
- atlassian-spring-scanner-maven-plugin
...
- – a helper plugin, responsible to scan your sources and create spring beans from annotations
...
You may can find more information about those these plugins in the Atlassian documentation site: https://developer.atlassian.com/
Now , having that you have a build file is fine, but there's a lot missing. Let's add those add the missing parts and let's start with all the support files needed.
Next, in In the resources directory of the we need to , create a META-INF/spring directory and in this directory we need to add a spring.xml file.
Expand |
---|
Code Block |
---|
| <?xml version="1.0" encoding="UTF-8"?>
<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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.atlassian.com/schema/atlassian-scanner
http://www.atlassian.com/schema/atlassian-scanner/atlassian-scanner.xsd">
<atlassian-scanner:scan-indexes />
<!--
- Please un-comment this to enable active object support in your plugin
-
<bean class="com.atlassian.activeobjects.external.TransactionalAnnotationProcessor">
<constructor-arg ref="activeObjects"/>
</bean>
-->
</beans> |
|
...
This adds support for bean scanning, allowing enabling you afterwards to annotate your components with stereotypes. Cool stuff, love Atlassian for this handy build tool.
Info |
---|
Starting with version 4.0.0, everything is a Spring Bean. Almost , almost everything. No more static dependencies, you need to rely on the injection mechanism. |
...
We will create two more files in the META-INF directory: KINFO and KVERSION . These files are required by the Kepler framework support and while KINFO is free text (you can put there anything you want), KVERSION has a fixed structure, it must contain inside the released date of the plugin:
...
Code Block |
---|
|
JIRA SIL Module: silexample, version ${version}
Simple SIL extension example.
Copyright (c) Kepler-Rominfo.
Visit us at http://www.kepler-rominfo.com |
There's still one important part missing: Another thing to work on is the atlassian-plugin.xml file (or the add-on descriptor). So let's create Create an empty add-on descriptor, like this:
Code Block |
---|
title | atlassian-plugin.xml |
---|
|
<atlassian-plugin key="com.mycompany.silexample"
name="SIL Example"
plugins-version="2">
<plugin-info>
<description>SIL Basic Example</description>
<version>${version}</version>
<vendor name="Kepler Rominfo" url="http://www.kepler-rominfo.com" />
<param name="plugin-icon">images/generic.png</param>
<param name="plugin-logo">images/generic.png</param>
<param name="vendor-icon">images/keplerLogo.png</param>
<param name="vendor-logo">images/keplerLogo.png</param>
<param name="atlassian-data-center-compatible">true</param>
</plugin-info>
</atlassian-plugin> |
...
Well, we have images for our add-on, but of course you can use whatever any images you want there as long as you respect the scaling of the icons. Or Alternatively, you can simply skip that the image part, it is really not that important.
In the end, the structure you will have will look like this:
Image Removed
You are now Image Added
Now you are ready to code.
The
...
basic skeleton: a launcher and the plugin info
Our framework require requires you to declare some certain basic information about what you bring to SIL. As you saw, some certain information may can be placed in descriptor files, but some other is declarative, in Java, since that info will get injected in into different places.
Open the editor, and create a new java class named ExamplePluginInfoService in the com.mycompany.silexample package.
Expand |
---|
Code Block |
---|
language | java |
---|
title | ExamplePluginInfoService.java |
---|
linenumbers | true |
---|
| /*
* File: ExamplePluginInfoService.java
* Created by rdumitriu on 13.12.2016.
*/
package com.mycompany.silexample;
import javax.annotation.Nonnull;
import com.keplerrominfo.refapp.config.PluginInfoService;
import org.springframework.stereotype.Component;
/**
* The plugin info service
*
* @author Radu Dumitriu (rdumitriu@gmail.com)
* @version 1.0
* @since 1.0
*/
@Component
public class ExamplePluginInfoService implements PluginInfoService {
@Nonnull
@Override
public String getKey() {
return "com.mycompany.silexample";
}
@Nonnull
@Override
public String getName() {
return "SIL Example";
}
}
|
|
...
The bean implementation is quite straight-forward. What you need to see is that this is This is a local component, not exported to the outside world; this is what the @Component annotation is used for. Makes sense, right ? This bean is local because it brings information
Now, we need to register some routines and CF descriptors, and we can do that at launch time of the plugin. For that, we need our own launcher. The start-stop sequence of plugins in complicated enough therefore we'll use our own Launcher class, abstracting all those events in such a way that you will know that whatever you do, it is safe to be done. So, lets extend / implement com.keplerrominfo.refapp.launcher.AbstractPluginLauncher: create a an ExampleLauncher class within the same package (com.mycompany.silexample). Override doAtLaunch() and doAtStop() members.
Expand |
---|
Code Block |
---|
language | java |
---|
title | ExampleLauncher.java |
---|
linenumbers | true |
---|
| /*
* File: ExampleLauncher.java
* Created by rdumitriu on 12.12.2016.
*/
package com.mycompany.silexample;
import com.atlassian.event.api.EventPublisher;
import com.atlassian.jira.issue.Issue;
import com.atlassian.jira.issue.fields.CustomField;
import com.atlassian.jira.project.ProjectManager;
import com.atlassian.plugin.spring.scanner.annotation.component.ClasspathComponent;
import com.atlassian.plugin.spring.scanner.annotation.export.ExportAsService;
import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
import com.atlassian.sal.api.lifecycle.LifecycleAware;
import com.keplerrominfo.jira.commons.ivm.*;
import com.keplerrominfo.refapp.config.*;
import com.keplerrominfo.refapp.config.impl.PluginConfigurationServiceImpl;
import com.keplerrominfo.refapp.launcher.AbstractPluginLauncher;
import com.keplerrominfo.sil.lang.RoutineRegistry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* This is a standard launcher for a Kepler add-on. We respect here the life-cycle of the add-on within the whole JIRA,
* so make sure you follow it.
*
* @author Radu Dumitriu (rdumitriu@gmail.com)
* @version 1.0
* @since 1.0
*/
@Component
@ExportAsService(LifecycleAware.class)
public class ExampleLauncher extends AbstractPluginLauncher {
private static final Log LOG = LogFactory.getLog(ExampleLauncher.class);
private final CustomFieldDescriptorRegistry customFieldDescriptorRegistry;
private final ProjectManager projectManager;
@Autowired
public ExampleLauncher(@ComponentImport EventPublisher eventPublisher,
//this is the local component providing info about the add-on (ExamplePluginInfoService)
PluginInfoService pluginInfoService,
//do not remove this, it will generate the correct component-import
@ComponentImport CommonPluginConfigurationService commonPluginConfigurationService,
@ComponentImport HostConfigurationProvider hostConfigurationProvider,
@ClasspathComponent PluginConfigurationServiceImpl pluginConfigurationService,
@ComponentImport CustomFieldDescriptorRegistry customFieldDescriptorRegistry,
@ComponentImport ProjectManager projectManager) {
super(eventPublisher, pluginInfoService, hostConfigurationProvider, pluginConfigurationService);
this.customFieldDescriptorRegistry = customFieldDescriptorRegistry;
this.projectManager = projectManager;
}
@Override
public void doAtLaunch() {
super.doAtLaunch();
//HERE:: will do more at launch
}
@Override
public void doAtStop() {
//HERE:: will do cleanup
super.doAtStop();
}
} |
|
I We have commented where you should put your own code. You should also note the beans annotated as @ComponentImport, as well as the local bean pluginConfigurationService, which which offers a separate space to hold configuration values (key-values pairs), local to this plugin.
...