JIRA Plugin Development——Configurable Custom Field Plugin

关于JIRA Plugin开发的中文资料相当少,这可能还是由于JIRA Plugin开发在国内比较小众的原因吧,下面介绍下自己的一个JIRA Plugin开发的详细过程。

业务需求

创建JIRA ISSUE时能提供一个字段,字段内容是类似于订单号或手机号这种样式的数据,并且显示出来是一个链接,点击后可跳转到订单详情页或手机号所对应的客户的整个订单页,供用户查看和此任务工单关联的订单数据;

例如:

订单号为123456;

订单详情URL为:http://192.168.11.211?order=123456;

则字段中显示出来的只能是123456的链接,而不是完整的URL,操作的用户是看不到整个链接地址的,不管是view还是edit界面,都不会显示URL地址,用户只需输入或修改订单号,保存后点击就可以直接跳转到订单详情页面;

解决办法

对于这种需求,JIRA自带的Custom Field Plugin就无法满足了,只能自己开发,开始没想到使用可配置的Custom Field,开始的解决办法是字段Value仍保存完整的URL,只是在显示和编辑时只让用户看到订单号,这样做有几个缺点,具体如下所示:

  • 必须在字段配置的Default Value中绑定URL前缀,拿上面的例子来说,就是http://192.168.11.211?order=,但是在显示和编辑时又不能让用户看到,只能在Velocity模板中去做一堆事情来完成,包括和默认URL前缀的匹配,js的处理等,限制性非常大;
  • 无法实现根据订单号的搜索,例如在Issue的Search for issues中搜索订单号为123456的issue就无法实现,因为字段值本身还是整个URL,而不是单纯的订单号;

身为程序员,自然不允许自己做出的东西是上面那样的残次品,于是研究了下可配置的Custom Field Plugin的实现过程;

关于Configurable Custom Field Plugin的参考资料相当少,具体实现参考了《Practical JIRA Plugins》第三章的一个例子;

可配置的字段,就是可以为字段添加一个配置项,在配置项中保存URL前缀,Value值只存储订单号,这样可以保证可按订单号搜索相关issue;

具体实现

实现Plugin的前提是我们的环境已经准备好了,即Atlassian的SDK包已经安装成功,并且本机Java环境的配置也已经OK,具体可参考:

https://developer.atlassian.com/docs/getting-started/set-up-the-atlassian-plugin-sdk-and-build-a-project

创建Plugin Project

切换到相应目录下,使用如下命令创建JIRA Plugin:

$ atlas-create-jira-plugin

会提示输入group-id,artifact-id,version,package,具体如下:

group-id  
com.mt.mcs.customfields
artifact-id  
configurableURL
version   
1.0-SNAPSHOT
package  
com.mt.mcs.customfields.configurableurl

 

 

 

 

 

 

group-id和artifact-id用来生成Plugin的唯一key,在本例中此Plugin的key为:com.mt.mcs.customfields.configurableurl;

version用在pom.xml中,并且是生成的.jar文件名种的一部分;

package是编写源码使用的Java包名;

之后会出现提示是否确认构建此Plugin,输入"Y"或"y"即可;

将项目导入IDE

我是用的是idea,操作很简单,只需Import Project—>当前Plugin的根目录(即pom.xml文件所在的目录),点击pom.xml后,点击导入,一路next即可(选择Java环境时记得选择你配置好的Java版本),具体可参考:https://developer.atlassian.com/docs/developer-tools/working-in-an-ide/configure-idea-to-use-the-sdk

如果使用Eclipse,可参考:https://developer.atlassian.com/docs/getting-started/set-up-the-atlassian-plugin-sdk-and-build-a-project/set-up-the-eclipse-ide-for-linux

修改pom.xml

添加你的组织或公司名称以及你网址的URL到<organization>,具体如下所示:

<organization>

    <name>Example Company</name>

    <url>http://www.example.com/</url>

</organization>

  修改<description>元素;

<description>This plugin is used for an URL which can config prefix.</description>

  添加customfield-type到atlassian-plugin.xml

添加完成后的atlassian-plugin.xml如下所示:

 1 <atlassian-plugin key="${project.groupId}.${project.artifactId}" name="${project.name}" plugins-version="2">

 2     <plugin-info>

 3         <description>${project.description}</description>

 4         <version>${project.version}</version>

 5         <vendor name="${project.organization.name}" url="${project.organization.url}" />

 6         <param name="plugin-icon">images/pluginIcon.png</param>

 7         <param name="plugin-logo">images/pluginLogo.png</param>

 8     </plugin-info>

 9 

10     <!-- add our i18n resource -->

11     <resource type="i18n" name="i18n" location="configurableURL"/>

12 

13     <!-- import from the product container -->

14     <component-import key="applicationProperties" interface="com.atlassian.sal.api.ApplicationProperties" />

15 

16     <customfield-type key="configurable-url"

17                       name="Configurable URL"

18                       class="com.mt.mcs.customfields.configurableurl.PrefixUrlCFType">

19         <description>

20             The Prefix URL Custom Field Type Plugin ...

21         </description>

22         <resource type="velocity"

23                   name="view"

24                   location="templates/com/mt/mcs/customfields/configurableurl/view.vm"></resource>

25         <resource type="velocity"

26                   name="edit"

27                   location="templates/com/mt/mcs/customfields/configurableurl/edit.vm"></resource>

28     </customfield-type>

29 </atlassian-plugin>

 

  第一行key="${project.groupId}.${project.artifactId}",表示此plugin的唯一标识;

  <customfield-type key="configurable-url" ...中的key为此customfield-type的唯一标识,要求在atlassian-plugin.xml中是唯一的;

name="Configurable URL",name为此custom field type在JIRA中显示的名字;

class="com.meituan.mcs.customfields.configurableurl.PrefixUrlCFType">,class为实现custom field type的Java类;

resource元素中包含了view和edit时,此字段使用的Velocity模板引擎;

创建CustomField Type的Class

现在我们需要创建一个Java类,实现CustomFieldType接口,并实现新的custom field type的各项功能,在类名末尾附加"CFType"是一个通用的约定,例如在我们的例子中,使用的Java类名为PrefixUrlCFType.java;

 代码如下所示:

 1 package com.mt.mcs.customfields.configurableurl;

 2 

 7 import com.atlassian.jira.issue.Issue;

 8 import com.atlassian.jira.issue.customfields.impl.FieldValidationException;

 9 import com.atlassian.jira.issue.customfields.impl.GenericTextCFType;

10 import com.atlassian.jira.issue.customfields.manager.GenericConfigManager;

11 import com.atlassian.jira.issue.customfields.persistence.CustomFieldValuePersister;

12 import com.atlassian.jira.issue.fields.CustomField;

13 import com.atlassian.jira.issue.fields.config.FieldConfig;

14 import com.atlassian.jira.issue.fields.config.FieldConfigItemType;

15 import com.atlassian.jira.issue.fields.layout.field.FieldLayoutItem;

16 

17 import java.util.List;

18 import java.util.Map;

19 import java.util.regex.Matcher;

20 import java.util.regex.Pattern;

21 

22 public class PrefixUrlCFType extends GenericTextCFType {

23 

24     public PrefixUrlCFType(CustomFieldValuePersister customFieldValuePersister, GenericConfigManager genericConfigManager) {

25         super(customFieldValuePersister, genericConfigManager);

26     }

27 

28     @Override

29     public List<FieldConfigItemType> getConfigurationItemTypes() {

30         final List<FieldConfigItemType> configurationItemTypes = super.getConfigurationItemTypes();

31         configurationItemTypes.add(new PrefixURLConfigItem());

32         return configurationItemTypes;

33     }

34 

35     @Override

36     public Map<String, Object> getVelocityParameters(final Issue issue,

37                                                      final CustomField field,

38                                                      final FieldLayoutItem fieldLayoutItem) {

39         final Map<String, Object> map = super.getVelocityParameters(issue, field, fieldLayoutItem);

40 

41         // This method is also called to get the default value, in

42         // which case issue is null so we can't use it to add currencyLocale

43         if (issue == null) {

44             return map;

45         }

46 

47         FieldConfig fieldConfig = field.getRelevantConfig(issue);

48         //add what you need to the map here50 

51         return map;

52     }

53 

54     public String getSingularObjectFromString(final String string) throws FieldValidationException

55     {

56         // JRA-14998 - trim the value.

57         final String value = (string == null) ? "Default" : string.trim();

58         if (value != null && value != "Default") {

59             Pattern p = Pattern.compile("^[0-9A-Za-z]+$");

60             Matcher m = p.matcher(value);

61             if (!m.matches()) {

62                 throw new FieldValidationException("Not Valid, only support a-z, A-Z and 0-9 ...");

63             }

64         }

65         return value;

66     }

67 }

添加配置项到Custom Field

对于每一个custom field,JIRA允许配置不同的内容,例如在不同的项目和任务类型中,select list字段就可以配置不同的option;

对于字段的配置项,我们首先要做的就是决定配置项中要存储什么值,在我们的项目中,存储的是URL前缀,使用字符串形式保存即可;

JIRA的配置项需要新定义一个类,并需要实现com.atlassian.jira.issue.fields.config.FieldConfigItemType接口,除此之外,我们还需要在JIRA中定义一个新的web页面,让我们填写并保存配置项的值;

代码如下所示:

 1 package com.meituan.mcs.customfields.configurableurl;

 2 

 3 import com.atlassian.jira.issue.Issue;

 4 import com.atlassian.jira.issue.fields.config.FieldConfig;

 5 import com.atlassian.jira.issue.fields.config.FieldConfigItemType;

 6 import com.atlassian.jira.issue.fields.layout.field.FieldLayoutItem;

 7 

 8 import java.util.HashMap;

 9 import java.util.Map;

10 

14 public class PrefixURLConfigItem implements FieldConfigItemType {

15 

16     @Override

17     //The name of this kind of configuration, as seen in the field configuration scheme;

18     public String getDisplayName() {

19         return "Config Prefix URL";

20     }

21 

22     @Override

23     // This is the text shown in the field configuration screen;

24     public String getDisplayNameKey() {

25         return "Prefix Of The URL";

26     }

27 

28     @Override

29     // This is the current value as shown in the field configuration screen

30     public String getViewHtml(FieldConfig fieldConfig, FieldLayoutItem fieldLayoutItem) {

31         String prefix_url = DAO.getCurrentPrefixURL(fieldConfig);

32         return prefix_url;

33     }

34 

35     @Override

36     //The unique identifier for this kind of configuration,

37     //and also the key for the $configs Map used in edit.vm

38     public String getObjectKey() {

39         return "PrefixUrlConfig";

40     }

41 

42     @Override

43     // Return the Object used in the Velocity edit context in $configs

44     public Object getConfigurationObject(Issue issue, FieldConfig fieldConfig) {

45         Map result = new HashMap();

46         result.put("prefixurl", DAO.getCurrentPrefixURL(fieldConfig));

47         return result;

48     }

49 

50     @Override

51     // Where the Edit link should redirect to when it's clicked on

52     public String getBaseEditUrl() {

53         return "EditPrefixUrlConfig.jspa";

54     }

55 }

DAO(Data Access Object)类的任务就是存储配置数据到数据库,具体数据存储先不在这里详细说明了,DAO类代码如下所示:

 1 package com.mt.mcs.customfields.configurableurl;

 2 

 3 import com.atlassian.jira.issue.fields.config.FieldConfig;

 4 import com.opensymphony.module.propertyset.PropertySet;

 5 import com.opensymphony.module.propertyset.PropertySetManager;

 6 import org.apache.log4j.Logger;

 7 

 8 import java.util.HashMap;

 9 

10 public class DAO {

11 

12     public static final Logger log;

13 

14     static {

15         log = Logger.getLogger(DAO.class);

16     }

17 

18     private static PropertySet ofbizPs = null;

19 

20     private static final int ENTITY_ID = 20000;

21 

22     private static PropertySet getPS() {

23         if (ofbizPs == null) {

24             HashMap ofbizArgs = new HashMap();

25             ofbizArgs.put("delegator.name", "default");

26             ofbizArgs.put("entityName", "prefix_url_fields");

27             ofbizArgs.put("entityId", new Long(ENTITY_ID));

28             ofbizPs = PropertySetManager.getInstance("ofbiz", ofbizArgs);

29         }

30         return ofbizPs;

31     }

32 

33     private static String getEntityName(FieldConfig fieldConfig) {

34         Long context = fieldConfig.getId();

35         String psEntityName = fieldConfig.getCustomField().getId() + "_" + context + "_config";

36         return psEntityName;

37     }

38 

39     public static String retrieveStoredValue(FieldConfig fieldConfig) {

40         String entityName = getEntityName(fieldConfig);

41         return getPS().getString(entityName);

42     }

43 

44     public static void updateStoredValue(FieldConfig fieldConfig, String value) {

45         String entityName = getEntityName(fieldConfig);

46         getPS().setString(entityName, value);

47     }

48 

49     public static String getCurrentPrefixURL(FieldConfig fieldConfig) {

50         String prefixurl = retrieveStoredValue(fieldConfig);

51         log.info("Current stored prefix url is " + prefixurl);

52         if (prefixurl == null || prefixurl.equals("")) {

53             prefixurl = null;

54         }

55         return prefixurl;

56     }

57 }

做完这些之后,还需要把PrefixURLConfigItem类和PrefixUrlCFType类关联起来,需要重写getConfigurationItemTypes方法,添加后的PrefixUrlCFType类如下所示:

 1 package com.mt.mcs.customfields.configurableurl;

 2 

 3 import com.atlassian.jira.issue.Issue;

 4 import com.atlassian.jira.issue.customfields.impl.FieldValidationException;

 5 import com.atlassian.jira.issue.customfields.impl.GenericTextCFType;

 6 import com.atlassian.jira.issue.customfields.manager.GenericConfigManager;

 7 import com.atlassian.jira.issue.customfields.persistence.CustomFieldValuePersister;

 8 import com.atlassian.jira.issue.fields.CustomField;

 9 import com.atlassian.jira.issue.fields.config.FieldConfig;

10 import com.atlassian.jira.issue.fields.config.FieldConfigItemType;

11 import com.atlassian.jira.issue.fields.layout.field.FieldLayoutItem;

12 

13 import java.util.List;

14 import java.util.Map;

15 import java.util.regex.Matcher;

16 import java.util.regex.Pattern;

17 

18 public class PrefixUrlCFType extends GenericTextCFType {

19 

20     public PrefixUrlCFType(CustomFieldValuePersister customFieldValuePersister, GenericConfigManager genericConfigManager) {

21         super(customFieldValuePersister, genericConfigManager);

22     }

23 

24     @Override

25     public List<FieldConfigItemType> getConfigurationItemTypes() {

26         final List<FieldConfigItemType> configurationItemTypes = super.getConfigurationItemTypes();

27         configurationItemTypes.add(new PrefixURLConfigItem());

28         return configurationItemTypes;

29     }

30 

31     @Override

32     public Map<String, Object> getVelocityParameters(final Issue issue,

33                                                      final CustomField field,

34                                                      final FieldLayoutItem fieldLayoutItem) {

35         final Map<String, Object> map = super.getVelocityParameters(issue, field, fieldLayoutItem);

36 

37         // This method is also called to get the default value, in

38         // which case issue is null so we can't use it to add currencyLocale

39         if (issue == null) {

40             return map;

41         }

42 

43         FieldConfig fieldConfig = field.getRelevantConfig(issue);

44         //add what you need to the map here

45         map.put("currentPrefixURL", DAO.getCurrentPrefixURL(fieldConfig));

46 

47         return map;

48     }

49 

50     public String getSingularObjectFromString(final String string) throws FieldValidationException

51     {

52         // JRA-14998 - trim the value.

53         final String value = (string == null) ? "Default" : string.trim();

54         if (value != null && value != "Default") {

55             Pattern p = Pattern.compile("^[0-9A-Za-z]+$");

56             Matcher m = p.matcher(value);

57             if (!m.matches()) {

58                 throw new FieldValidationException("Not Valid, only support a-z, A-Z and 0-9 ...");

59             }

60         }

61         return value;

62     }

63 }

 Velocity模板引擎

custom field在JIRA中显示和编辑,需要使用Velocity模板,即view.vm和edit.vm,具体如下所示:

view.vm

 1 #disable_html_escaping()

 2 #set($defaultValue = "Default")

 3 #if ($value && $value != $defaultValue)

 4     #if ($currentPrefixURL)

 5         <a class="tinylink" target="_blank" href="$currentPrefixURL$value">$!textutils.htmlEncode($value)</a>

 6     #else

 7         #set($displayValue = "没有配置URL前缀...")

 8         $!textutils.htmlEncode($displayValue)

 9     #end

10 #elseif ($value == $defaultValue)

11     #set($displayValue = "请输入相关信息...")

12     $textutils.htmlEncode($displayValue)

13 #else

14     #set($displayValue = "出现错误了....")

15     $textutils.htmlEncode($displayValue)

16 #end

edit.vm

 1 #disable_html_escaping()

 2 #customControlHeader ($action $customField.id $customField.name $fieldLayoutItem.required $displayParameters $auiparams)

 3 #set($configObj = $configs.get("PrefixUrlConfig"))

 4 #set($prefixUrl = $configObj.get("prefixurl"))

 5 #set($defaultValue = "Default")

 6 #if ($value == $defaultValue)

 7     <input class="text" id="displayText" name="displayText" type="text" value="" onchange="changeValue(${customField.id})">

 8     <input class="text" id="$customField.id" name="$customField.id" type="hidden" value="$textutils.htmlEncode($!value)">

 9 #else

10     <input class="text" id="$customField.id" name="$customField.id" type="text" value="$textutils.htmlEncode($!value)">

11 #end

12 <script type="text/javascript">

13   function changeValue(cfElmId) {

14     var cfElmId = cfElmId.id;

15     var element = document.getElementById("displayText");

16     var elmVal = element.value;

17     var cfElm = document.getElementById(cfElmId);

18     cfElm.value = elmVal;

19   }

20 </script>

21 #customControlFooter ($action $customField.id $fieldLayoutItem.fieldDescription $displayParameters $auiparams)

WebWork Action

到现在为止,我们定义了一个新类型的配置项,并且更新了PrefixUrlCFType类和Velocity模板引擎,我们还需要一个新的web页面,来设置配置项的值(即URL前缀信息)并保存到数据库;

JIRA是通过WebWork web应用框架来定义web页面的,需要在atlassian-plugin.xml文件中配置webwork元素,具体如下所示:

 1 <webwork1 key="url-configurable"

 2           name="URL configuration action"

 3           class="java.lang.Object">

 4     <description>

 5         The action for editing a prefix url custom field type configuration.

 6     </description>

 7     <actions>

 8         <action name="com.mt.mcs.customfields.configurableurl.EditPrefixUrlConfig"

 9                 alias="EditPrefixUrlConfig">

10             <view name="input">

11                 /templates/com/mt/mcs/customfields/configurableurl/edit-config.vm

12             </view>

13             <view name="securitybreach">

14                 /secure/views/securitybreach.jsp

15             </view>

16         </action>

17     </actions>

18 </webwork1>

使用的edit-config.vm模板文件代码如下所示:

 1 <html>

 2 <head>

 3   <title>

 4       $i18n.getText('common.words.configure')

 5       $action.getCustomField().getName()

 6   </title>

 7   <meta content="admin" name="decorator">

 8   <link rel="stylesheet" type="text/css" media="print" href="/styles/combined-printtable.css">

 9   <link rel="stylesheet" type="text/css" media="all" href="/styles/combined.css">

10   <style>

11     table.base-table {

12       margin: 15px auto;

13       border-spacing: 5px 10px;

14       line-height: 1.5;

15       font-size: 16px;

16     }

17     input.prefixurl {

18       outline: none;

19       box-shadow: bisque;

20       width: 350px !important;

21     }

22     table.base-table input#Save {

23       margin-left: 80px;

24     }

25   </style>

26 </head>

27 <body>

28 <h2 class="formtitle">

29     $i18n.getText('common.words.configure') $action.getCustomField().getName()

30 </h2>

31 <div class="aui-message aui-message-info">

32   <p class="title">

33     <span class="aui-icon icon-info"></span>

34     <strong>Notice</strong>

35   </p>

36   <p>

37     Config the prefix of your URL.

38   </p>

39   <p>

40     At the end of the URL, you need to add a '/', such as 'http://192.168.11.234/' !

41   </p>

42 </div>

43 <form action="EditPrefixUrlConfig.jspa" method="post" class="aui">

44   <table class="base-table">

45     <tr>

46       <td>

47         Prefix Url:&nbsp;

48       </td>

49       <td>

50         #set($prefix_url = $action.getPrefixurl())

51         <input type="text" name="prefixurl" id="prefixurl" value="$!prefix_url" class="text prefixurl">

52       </td>

53     </tr>

54     <tr>

55       <td colspan="2">

56         <input type="submit" name="Save" id="Save" value="$i18n.getText('common.words.save')" class="aui-button">

57         <a href="ConfigureCustomField!default.jspa?customFieldId=$action.getCustomField().getIdAsLong().toString()"

58            id="cancelButton" class="aui-button" name="ViewCustomFields.jspa">

59           Cancel

60         </a>

61       </td>

62     </tr>

63   </table>

64   <input type="hidden" name="fieldConfigId" value="$fieldConfigId">

65 </form>

66 </body>

67 </html>

Action Class

配置项的web页面使用的Action类是EditPrefixUrlConfig.java,代码如下所示:

 1 package com.mt.mcs.customfields.configurableurl;

 2 

 3 import com.atlassian.jira.config.managedconfiguration.ManagedConfigurationItemService;

 4 import com.atlassian.jira.issue.customfields.impl.FieldValidationException;

 5 import com.atlassian.jira.security.Permissions;

 6 import com.atlassian.jira.web.action.admin.customfields.AbstractEditConfigurationItemAction;

 7 import com.opensymphony.util.UrlUtils;

 8 

 9 public class EditPrefixUrlConfig extends AbstractEditConfigurationItemAction {

10 

11     protected EditPrefixUrlConfig(ManagedConfigurationItemService managedConfigurationItemService) {

12         super(managedConfigurationItemService);

13     }

14 

15     private String prefixurl;

16 

17     public void setPrefixurl(String prefixurl) {

18         this.prefixurl = prefixurl;

19     }

20 

21     public String getPrefixurl() {

22         return this.prefixurl;

23     }

24 

25     protected void doValidation() {

26         String prefix_url = getPrefixurl();

27         prefix_url = (prefix_url == null) ? null : prefix_url.trim();

28         if (prefix_url == null) {

29             return;

30         }

31         if (!UrlUtils.verifyHierachicalURI(prefix_url)) {

32             addErrorMessage("ERROR: " + prefix_url + " is not a valid URL...");

33         }

34     }

35 

36     protected String doExecute() throws Exception {

37         if (!isHasPermission(Permissions.ADMINISTER)) {

38             return "securitybreach";

39         }

40         if (getPrefixurl() == null) {

41             setPrefixurl(DAO.retrieveStoredValue(getFieldConfig()));

42         }

43         DAO.updateStoredValue(getFieldConfig(), getPrefixurl());

44         String save = request.getParameter("Save");

45         if (save != null && save.equals("Save")) {

46             setReturnUrl("/secure/admin/ConfigureCustomField!default.jspa?customFieldId=" + getFieldConfig().getCustomField().getIdAsLong().toString());

47             return getRedirect("not used");

48         }

49         return INPUT;

50     }

51 }

这样整个可配置的Custom Field Plugin已经正式开发完成了,只是搜索功能还没有实现,搜索只是继承已有的Searcher即可,本例继承的是TextSearcher;

Searcher的实现可参考:https://www.safaribooksonline.com/library/view/practical-jira-plugins/9781449311322/ch04.html,讲解非常详细;

你可能感兴趣的:(plugin)