※クラウド版はこの手順の対象外。クラウド版はこちらを参照。
Atlassianのデベロッパーサイトに情報はある。
しかし、基本部分だけがまとまっている情報源があまりないので、最低限の簡単なアドオンをつくる流れをメモします。
Eclipseは使いません。Javaをしらなくても、HTML, CSS, JavaScriptがわかれば開発できる流れです。
Javaの部分はREST APIでデータアクセスだけにし、その他はフロントエンドで開発する場合にはこれで十分なはずです。
1. 環境準備
公式サイト:Set up the Atlassian Plugin SDK and Build a Project
簡単には、次のとおり。
- Java 8のインストール:Oracle JDK 8 Downloads
- Atlassian SDKのインストール:Mac, Windows
2. スケルトンの作成
公式サイト:Create the plugin skeleton
atlas-create-refapp-pluginコマンド
refappでつくれば、Atlassianのどの製品でも動作する。
$ atlas-create-refapp-plugin
次の入力が求められるので、入力する。
groupIdとpackageはcom.atlassian.plugins.tutorial.refapp
のように、artifactIdはアドオン名、versionは 1.0-SNAPSHOT
のように。
packageはJavaのパッケージになるので注意。
- groupId
- artifactId
- version
- package
自動生成された不要ファイルの削除
次の配下のファイルは不要なので削除する(再利用してもよい)。
src/test/java
src/test/resources
src/main/java/指定したパッケージ
動作確認(atlas-run)
スケルトン作成で生成されたディレクトリに移動して、atlas-run
コマンドを実行する。
$ atlas-clean
$ atlas-run --product jira --version 7.3.6
コンソールのメッセージに表示されるとおり、URLにアクセスする。
http://localhost:2990/jira/plugins/servlet/developer-toolbox
Username: admin
Password: admin
Ctrl-Dで停止する。
3. Eclipseの環境設定
簡単なアドオン開発には不要なので省略。
使いたい人は公式サイトのとおり。
4. 単純なServletの作成
公式サイト: Convert component to servlet module
Servletの作成
スケルトン作成で指定したパッケージ配下に以下のようにServletを作成する。
(クラス名、パッケージは適宜修正)
package com.atlassian.plugins.tutorial.refapp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class MyPluginServlet extends HttpServlet
{
private static final Logger log = LoggerFactory.getLogger(MyPluginServlet.class);
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
resp.setContentType("text/html");
resp.getWriter().write("<html><body>Hello! You did it.</body></html>");
}
}
Servletの登録
atlassian-plugin.xml
のservlet
タグを以下のように追加する。
<servlet name="adminUI" class="com.atlassian.plugins.tutorial.refapp.MyPluginServlet" key="test">
<url-pattern>/test</url-pattern>
</servlet>
動作確認
$ atlas-clean
$ atlas-mvn package
$ atlas-run --product jira --version 7.3.6
5. VelocityテンプレートとAUIを使ったServletの作成
公式サイト: Create a GUI with templates and AUI
VelocityはJavaのテンプレート。
AUIはAtlassian User InterfaceでCSSのフレームワークのようなもの。
AUIのインストール
pom.xml
のdependenciesの中に以下を追加する。
<dependency>
<groupId>com.atlassian.templaterenderer</groupId>
<artifactId>atlassian-template-renderer-api</artifactId>
<scope>provided</scope>
</dependency>
Velocityファイルの作成
src/main/resources
配下に次の例のようなVelocityファイルを作成する。
<html>
<head>
<title>MyServlet Admin</title>
<meta name="decorator" content="atl.admin">
</head>
<body>
<form id="admin" class="aui" action="" method="POST">
<div class="field-group">
<label for="name">Name:</label>
<input type="text" id="name" name="name" class="text">
</div>
<div class="field-group">
<label for="age">Age:</label>
<input type="text" id="age" name="age" class="text">
</div>
<div class="field-group">
<input type="submit" value="Save" class="button">
</div>
</form>
</body>
</html>
Servletの修正
次の例のようにServletを修正する。
(クラス名、パッケージは適宜修正)
doGetの中がgetリクエストの際に処理される部分。
ユーザー名と管理者権限を確認して、OKならadmin.vm
を返している。
権限チェックの部分などは適宜削除し、admin.vm
部分は自分が作成したVelocityファイル名に変更する。
package com.atlassian.plugins.tutorial.refapp;
import javax.servlet.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
import com.atlassian.plugin.spring.scanner.annotation.component.Scanned;
import javax.inject.Inject;
import java.net.URI;
import com.atlassian.sal.api.auth.LoginUriProvider;
import com.atlassian.sal.api.user.UserManager;
import com.atlassian.templaterenderer.TemplateRenderer;
@Scanned
public class MyPluginServlet extends HttpServlet
{
@ComponentImport
private final UserManager userManager;
@ComponentImport
private final LoginUriProvider loginUriProvider;
@ComponentImport
private final TemplateRenderer templateRenderer;
@Inject
public MyPluginServlet(UserManager userManager, LoginUriProvider loginUriProvider, TemplateRenderer templateRenderer)
{
this.userManager = userManager;
this.loginUriProvider = loginUriProvider;
this.templateRenderer = templateRenderer;
}
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
String username = userManager.getRemoteUsername(request);
if (username == null || !userManager.isSystemAdmin(username))
{
redirectToLogin(request, response);
return;
}
templateRenderer.render("admin.vm", response.getWriter());
}
private void redirectToLogin(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.sendRedirect(loginUriProvider.getLoginUri(getUri(request)).toASCIIString());
}
private URI getUri(HttpServletRequest request)
{
StringBuffer builder = request.getRequestURL();
if (request.getQueryString() != null)
{
builder.append("?");
builder.append(request.getQueryString());
}
return URI.create(builder.toString());
}
// This is what your MyPluginServlet.java class should look like after creating your admin.vm template
}
動作確認
atlas-runは停止していなければ再起動しなくても、atlas-mvnコマンドをすれば自動でリロードされる。
$ atlas-clean
$ atlas-mvn package
$ atlas-run --product jira --version 7.3.6
6. postとデータの保存
公式サイト:Store and retrieve plugin data
データベースはキー・バリューストア。
Servletの修正
次の例のようにServletを修正する。
(クラス名、パッケージ、Velocityファイル名、PLUGIN_STORAGE_KEYの値は適宜修正)
pluginSettings部分がデータベースにアクセスしている部分。
doGetはget, doPostはputしているのがわかる。
package com.atlassian.plugins.tutorial.refapp;
import java.util.HashMap;
import java.util.Map;
import java.io.IOException;
import java.net.URI;
import javax.servlet.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
import com.atlassian.plugin.spring.scanner.annotation.component.Scanned;
import javax.inject.Inject;
import com.atlassian.sal.api.auth.LoginUriProvider;
import com.atlassian.sal.api.user.UserManager;
import com.atlassian.templaterenderer.TemplateRenderer;
import com.atlassian.sal.api.pluginsettings.PluginSettings;
import com.atlassian.sal.api.pluginsettings.PluginSettingsFactory;
@Scanned
public class MyPluginServlet extends HttpServlet
{
private static final String PLUGIN_STORAGE_KEY = "com.atlassian.plugins.tutorial.refapp.adminui";
@ComponentImport
private final UserManager userManager;
@ComponentImport
private final LoginUriProvider loginUriProvider;
@ComponentImport
private final TemplateRenderer templateRenderer;
@ComponentImport
private final PluginSettingsFactory pluginSettingsFactory;
@Inject
public MyPluginServlet(UserManager userManager, LoginUriProvider loginUriProvider, TemplateRenderer templateRenderer, PluginSettingsFactory pluginSettingsFactory) {
this.userManager = userManager;
this.loginUriProvider = loginUriProvider;
this.templateRenderer = templateRenderer;
this.pluginSettingsFactory = pluginSettingsFactory;
}
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
String username = userManager.getRemoteUsername(request);
if (username == null || !userManager.isSystemAdmin(username))
{
redirectToLogin(request, response);
return;
}
Map<String, Object> context = new HashMap<String, Object>();
PluginSettings pluginSettings = pluginSettingsFactory.createGlobalSettings();
if (pluginSettings.get(PLUGIN_STORAGE_KEY + ".name") == null){
String noName = "Enter a name here.";
pluginSettings.put(PLUGIN_STORAGE_KEY +".name", noName);
}
if (pluginSettings.get(PLUGIN_STORAGE_KEY + ".age") == null){
String noAge = "Enter an age here.";
pluginSettings.put(PLUGIN_STORAGE_KEY + ".age", noAge);
}
context.put("name", pluginSettings.get(PLUGIN_STORAGE_KEY + ".name"));
context.put("age", pluginSettings.get(PLUGIN_STORAGE_KEY + ".age"));
response.setContentType("text/html;charset=utf-8");
templateRenderer.render("admin.vm", response.getWriter());
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse response)
throws ServletException, IOException {
PluginSettings pluginSettings = pluginSettingsFactory.createGlobalSettings();
pluginSettings.put(PLUGIN_STORAGE_KEY + ".name", req.getParameter("name"));
pluginSettings.put(PLUGIN_STORAGE_KEY + ".age", req.getParameter("age"));
response.sendRedirect("test");
}
private void redirectToLogin(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.sendRedirect(loginUriProvider.getLoginUri(getUri(request)).toASCIIString());
}
private URI getUri(HttpServletRequest request)
{
StringBuffer builder = request.getRequestURL();
if (request.getQueryString() != null)
{
builder.append("?");
builder.append(request.getQueryString());
}
return URI.create(builder.toString());
}
// This is what your MyPluginServlet.java should look like in its final stages.
}
動作確認
画面でsubmitし、値をpostしてみる。
$ atlas-clean
$ atlas-mvn package
$ atlas-run --product jira --version 7.3.6
その後、Plugin Data Editorでデータを確認する。
7. メニューの作成
公式サイト:Modify the Plugin
atlassian-plugin.xml
の<atlassian-plugin>
タグの中に次のようにメニューの情報を追加する。
web-section
はメニューのグループのようなもので実態はない。
web-item
がメニューの実体。
次の例の場合、グローバルメニューをクリックすると、その配下に2つのメニューがあらわれる。
- T4TItemがナビゲーションバーに表示される(sectionに注意)
- T4TSectionはT4TItemに紐付けられるグループ(locationに注意)
- D4DItemとCalendarItemはT4TSectionの中に入る(sectionに注意)
<web-item name="T4TItem" i18n-name-key="t4t.name" key="t4t-item" section="system.top.navigation.bar" weight="1000">
<description key="t4t.description">The T4T Plugin</description>
<label key="T4T" />
<link linkId="t4t-link"></link>
</web-item>
<web-section name="T4TSection" i18n-name-key="t4t.name" key="t4t-section" location="t4t-link" weight="1000">
<description key="t4t.description">The T4T Plugin</description>
</web-section>
<web-item name="D4DItem" i18n-name-key="d4d.name" key="d4d-item" section="t4t-link/t4t-section" weight="1000">
<description key="d4d.description">The D4D Plugin</description>
<label key="D4D" />
<link linkId="d4d-link">/plugins/servlet/t4t/d4d</link>
</web-item>
<web-item name="CalendarItem" i18n-name-key="calendar.name" key="calendar-item" section="t4t-link/t4t-section" weight="2000">
<description key="calendar.description">The Calendar Plugin</description>
<label key="Calendar" />
<link linkId="calendar-link">/plugins/servlet/t4t/calendar</link>
</web-item>
8. CSS, JavaScript, 画像の設定
公式サイト:Custom Fields that use CSS or JavaScript Web Resources in JIRA 5.0
Webリソースの定義
atlassian.xml
の<!-- add our web resources -->
部分を次のように修正する。
key
とresource
のname
, location
は適宜修正する。
<web-resource key="d4d-resources" name="d4d Web Resources">
<dependency>com.atlassian.auiplugin:ajs</dependency>
<resource type="download" name="d4d.css" location="/css/d4d.css"/>
<resource type="download" name="d4d.js" location="/js/d4d.js"/>
<resource type="download" name="images/" location="/images"/>
<context>t4t</context>
</web-resource>
Velocityファイルの修正
Velocityファイルの1行目に次を追加する。
:
の前の部分はatlassian-pluginのkeyなので、スケルトン作成時の[groupId].[artifactId]
だが、不明ならば、atlas-mvn package
した後に作成されるtarget/classes
配下のatlassian-plugin.xml
ファイルをみればわかる。
$webResourceManager.requireResource("id.au.penny.jiracontexts.jira-context-plugin:resources")
これで、atlassian.xmlで指定したCSS, Javascript, 画像が使える。
なお、jQueryはインストールしなくてもAUIに同梱されているため使える。
9. REST APIの作成
「4. 単純なServletの作成」の部分を応用すれば、REST APIも簡単につくれる。
そうすると、Javaで作成する部分はREST APIだけにして、あとはフロントで作ればよい。
REST API用Servletの作成
次の例のように、doGet, doPost, doPut, doDeleteを定義する。
ポイントは、org.json.JSONObjectとorg.json.JSONArrayを使ってJSONを扱っているところ。
package itagaki.shintaro;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
import java.io.IOException;
import java.net.URI;
import javax.servlet.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
import com.atlassian.plugin.spring.scanner.annotation.component.Scanned;
import javax.inject.Inject;
import com.atlassian.sal.api.auth.LoginUriProvider;
import com.atlassian.sal.api.user.UserManager;
import com.atlassian.templaterenderer.TemplateRenderer;
import com.atlassian.sal.api.pluginsettings.PluginSettings;
import com.atlassian.sal.api.pluginsettings.PluginSettingsFactory;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONArray;
import java.util.List;
import java.util.stream.Collectors;
@Scanned
public class D4DApiServlet extends HttpServlet {
private static final String PLUGIN_STORAGE_KEY = "itagaki.shintaro.t4t.d4d";
@ComponentImport
private final UserManager userManager;
@ComponentImport
private final LoginUriProvider loginUriProvider;
@ComponentImport
private final TemplateRenderer templateRenderer;
@ComponentImport
private final PluginSettingsFactory pluginSettingsFactory;
@Inject
public D4DApiServlet(UserManager userManager, LoginUriProvider loginUriProvider, TemplateRenderer templateRenderer, PluginSettingsFactory pluginSettingsFactory) {
this.userManager = userManager;
this.loginUriProvider = loginUriProvider;
this.templateRenderer = templateRenderer;
this.pluginSettingsFactory = pluginSettingsFactory;
}
private static final Logger log = LoggerFactory.getLogger(D4DApiServlet.class);
@Override
public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException {
PluginSettings pluginSettings = pluginSettingsFactory.createGlobalSettings();
res.setContentType("application/json;charset=utf-8");
if ( pluginSettings.get(PLUGIN_STORAGE_KEY + ".themes") == null) {
res.getWriter().write("[]");
} else {
log.info( (String)pluginSettings.get(PLUGIN_STORAGE_KEY + ".themes") );
res.getWriter().write( (String)pluginSettings.get(PLUGIN_STORAGE_KEY + ".themes") );
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
PluginSettings pluginSettings = pluginSettingsFactory.createGlobalSettings();
res.setContentType("application/json;charset=utf-8");
if ( req.getParameter("theme") == null) {
res.getWriter().write("{ Error: 'need to theme'}");
} else {
JSONObject theme = new JSONObject(req.getParameter("theme"));
String themes;
if ( pluginSettings.get(PLUGIN_STORAGE_KEY + ".themes") == null) {
themes = "[]";
} else {
themes = pluginSettings.get(PLUGIN_STORAGE_KEY + ".themes").toString();
}
JSONArray themesArray = new JSONArray(themes);
themesArray.put(theme);
pluginSettings.put(PLUGIN_STORAGE_KEY + ".themes", themesArray.toString() );
res.getWriter().write(req.getParameter("theme"));
}
}
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
PluginSettings pluginSettings = pluginSettingsFactory.createGlobalSettings();
res.setContentType("application/json;charset=utf-8");
if ( req.getParameter("id") == null) {
res.getWriter().write("{ Error: 'need to id'}");
} else {
String id = req.getParameter("id");
String themes;
if ( pluginSettings.get(PLUGIN_STORAGE_KEY + ".themes") == null) {
res.getWriter().write("not exists");
} else {
themes = pluginSettings.get(PLUGIN_STORAGE_KEY + ".themes").toString();
JSONArray themesArray = new JSONArray(themes);
for( int i=0; i<themesArray.length(); i++ ) {
if( id.equals( themesArray.getJSONObject(i).get("id").toString() ) ) {
themesArray.remove(i);
}
}
pluginSettings.put(PLUGIN_STORAGE_KEY + ".themes", themesArray.toString() );
res.getWriter().write( id );
}
}
}
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
log.info("===================");
log.info(req.getParameter("newTheme"));
PluginSettings pluginSettings = pluginSettingsFactory.createGlobalSettings();
res.setContentType("application/json;charset=utf-8");
if ( req.getParameter("newTheme") == null) {
res.getWriter().write("{ Error: 'need to newTheme'}");
} else {
JSONObject newTheme = new JSONObject(req.getParameter("newTheme"));
String themes;
if ( pluginSettings.get(PLUGIN_STORAGE_KEY + ".themes") == null) {
themes = "[]";
} else {
themes = pluginSettings.get(PLUGIN_STORAGE_KEY + ".themes").toString();
}
JSONArray themesArray = new JSONArray(themes);
for( int i=0; i<themesArray.length(); i++ ) {
if( newTheme.get("id").toString().equals( themesArray.getJSONObject(i).get("id").toString() ) ) {
themesArray.remove(i);
}
}
themesArray.put(newTheme);
pluginSettings.put(PLUGIN_STORAGE_KEY + ".themes", themesArray.toString() );
res.getWriter().write(req.getParameter("newTheme"));
}
}
}
Servletの登録
「4. 単純なServletの作成」と同様
注意点
jQueryのajaxでAPIにデータを送信するときは次の2点に注意。
- JSONデータはJSON.stringifyする
- PutとDeleteのデータはクエリストリングで指定する。
var getThemes = function() {
$.getJSON( apiUrl )
.done( function( d ) {
console.log( 'get done', d );
themes = d;
updateThemesVue();
} )
.fail( function( d ) {
console.log( 'get error', d );
} );
}
var addTheme = function( theme ) {
$.post( apiUrl, { theme: JSON.stringify( theme ) } )
.done( function( d ) {
console.log( 'upload done' + d );
themes.push( theme );
updateThemesVue();
} )
.fail( function( d ) {
console.log( 'upload error', d );
} );
}
var removeTheme = function( id ) {
$.ajax( {
type: "DELETE",
url: apiUrl + '?' + $.param( { id: id } )
} )
.done( function( d ) {
console.log( 'remove done' + d );
themes = themes.filter( function( theme ) { return theme.id !== id; } );
updateThemesVue();
} )
.fail( function( d ) {
console.log( 'remove error', d );
} );
}
var updateTheme = function( newTheme ) {
$.ajax( {
type: "PUT",
url: apiUrl + '?' + $.param( { newTheme: JSON.stringify( newTheme ) } )
} )
.done( function( d ) {
console.log( 'update done' + d );
themes = themes.map( function( theme ) {
if ( theme.id === newTheme.id ) {
return newTheme;
} else {
return theme;
}
} );
updateThemesVue();
} )
.fail( function( d ) {
console.log( 'update error', d );
} );
}
まとめ
コマンド
$ atlas-create-refapp-plugin
$ atlas-clean
$ atlas-mvn package
$ atlas-run --product jira --version 7.3.6
url
http://localhost:2990/jira/plugins/servlet/developer-toolbox
admin:admin
pom.xml
AUIのインストール
<dependency>
<groupId>com.atlassian.templaterenderer</groupId>
<artifactId>atlassian-template-renderer-api</artifactId>
<scope>provided</scope>
</dependency>
atlassian-plugin.xml
- Servletの登録:servletタグ
- リソースの定義:web-resourceタグ
- メニューの定義:web-sectionタグ、web-itemタグ
サーブレットの作成
- doXXXでhttpメソッドに対応
- templateRendererでvelocityを指定
- pluginSettingsでデータベース(キー・バリューストア)にアクセス
Velocityの作成
- AUIを使って装飾
- webResourceManager.requireResourceでリソース(CSS, JS, 画像)を使えるようにする