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,讲解非常详细;

posted @ 2015-02-09 00:01  冰轮封雪  阅读(2382)  评论(1编辑  收藏  举报