6
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

JIRA Serverのアドオン開発入門

Last updated at Posted at 2017-05-14

※クラウド版はこの手順の対象外。クラウド版はこちらを参照。
Atlassianのデベロッパーサイトに情報はある。
しかし、基本部分だけがまとまっている情報源があまりないので、最低限の簡単なアドオンをつくる流れをメモします。

Eclipseは使いません。Javaをしらなくても、HTML, CSS, JavaScriptがわかれば開発できる流れです。
Javaの部分はREST APIでデータアクセスだけにし、その他はフロントエンドで開発する場合にはこれで十分なはずです。

1. 環境準備

公式サイト:Set up the Atlassian Plugin SDK and Build a Project

簡単には、次のとおり。

  1. Java 8のインストール:Oracle JDK 8 Downloads
  2. 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を作成する。
(クラス名、パッケージは適宜修正)

MyPluginServlet.java
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.xmlservletタグを以下のように追加する。

atlassian-plugin.xml
<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の中に以下を追加する。

pom.xml
<dependency>
  		<groupId>com.atlassian.templaterenderer</groupId>
  		<artifactId>atlassian-template-renderer-api</artifactId>
  		<scope>provided</scope>
</dependency>

Velocityファイルの作成

src/main/resources配下に次の例のようなVelocityファイルを作成する。

admin.vm
<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ファイル名に変更する。

MyPluginServlet.java
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しているのがわかる。

MyPluginServlet.java
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に注意)
atlassian-plugin.xml
  <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 -->部分を次のように修正する。
keyresourcename, locationは適宜修正する。

atlassian.xml
  <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を扱っているところ。

D4DApiServlet.java
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のインストール

pom.xml
<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, 画像)を使えるようにする
6
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?