关于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如下所示:

 <atlassian-plugin key="${project.groupId}.${project.artifactId}" name="${project.name}" plugins-version="2">
<plugin-info>
<description>${project.description}</description>
<version>${project.version}</version>
<vendor name="${project.organization.name}" url="${project.organization.url}" />
<param name="plugin-icon">images/pluginIcon.png</param>
<param name="plugin-logo">images/pluginLogo.png</param>
</plugin-info> <!-- add our i18n resource -->
<resource type="i18n" name="i18n" location="configurableURL"/> <!-- import from the product container -->
<component-import key="applicationProperties" interface="com.atlassian.sal.api.ApplicationProperties" /> <customfield-type key="configurable-url"
name="Configurable URL"
class="com.mt.mcs.customfields.configurableurl.PrefixUrlCFType">
<description>
The Prefix URL Custom Field Type Plugin ...
</description>
<resource type="velocity"
name="view"
location="templates/com/mt/mcs/customfields/configurableurl/view.vm"></resource>
<resource type="velocity"
name="edit"
location="templates/com/mt/mcs/customfields/configurableurl/edit.vm"></resource>
</customfield-type>
</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;

代码如下所示:

 package com.mt.mcs.customfields.configurableurl;

 import com.atlassian.jira.issue.Issue;
import com.atlassian.jira.issue.customfields.impl.FieldValidationException;
import com.atlassian.jira.issue.customfields.impl.GenericTextCFType;
import com.atlassian.jira.issue.customfields.manager.GenericConfigManager;
import com.atlassian.jira.issue.customfields.persistence.CustomFieldValuePersister;
import com.atlassian.jira.issue.fields.CustomField;
import com.atlassian.jira.issue.fields.config.FieldConfig;
import com.atlassian.jira.issue.fields.config.FieldConfigItemType;
import com.atlassian.jira.issue.fields.layout.field.FieldLayoutItem; import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern; public class PrefixUrlCFType extends GenericTextCFType { public PrefixUrlCFType(CustomFieldValuePersister customFieldValuePersister, GenericConfigManager genericConfigManager) {
super(customFieldValuePersister, genericConfigManager);
} @Override
public List<FieldConfigItemType> getConfigurationItemTypes() {
final List<FieldConfigItemType> configurationItemTypes = super.getConfigurationItemTypes();
configurationItemTypes.add(new PrefixURLConfigItem());
return configurationItemTypes;
} @Override
public Map<String, Object> getVelocityParameters(final Issue issue,
final CustomField field,
final FieldLayoutItem fieldLayoutItem) {
final Map<String, Object> map = super.getVelocityParameters(issue, field, fieldLayoutItem); // This method is also called to get the default value, in
// which case issue is null so we can't use it to add currencyLocale
if (issue == null) {
return map;
} FieldConfig fieldConfig = field.getRelevantConfig(issue);
//add what you need to the map here
return map;
} public String getSingularObjectFromString(final String string) throws FieldValidationException
{
// JRA-14998 - trim the value.
final String value = (string == null) ? "Default" : string.trim();
if (value != null && value != "Default") {
Pattern p = Pattern.compile("^[0-9A-Za-z]+$");
Matcher m = p.matcher(value);
if (!m.matches()) {
throw new FieldValidationException("Not Valid, only support a-z, A-Z and 0-9 ...");
}
}
return value;
}
}

添加配置项到Custom Field

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

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

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

代码如下所示:

 package com.meituan.mcs.customfields.configurableurl;

 import com.atlassian.jira.issue.Issue;
import com.atlassian.jira.issue.fields.config.FieldConfig;
import com.atlassian.jira.issue.fields.config.FieldConfigItemType;
import com.atlassian.jira.issue.fields.layout.field.FieldLayoutItem; import java.util.HashMap;
import java.util.Map; public class PrefixURLConfigItem implements FieldConfigItemType { @Override
//The name of this kind of configuration, as seen in the field configuration scheme;
public String getDisplayName() {
return "Config Prefix URL";
} @Override
// This is the text shown in the field configuration screen;
public String getDisplayNameKey() {
return "Prefix Of The URL";
} @Override
// This is the current value as shown in the field configuration screen
public String getViewHtml(FieldConfig fieldConfig, FieldLayoutItem fieldLayoutItem) {
String prefix_url = DAO.getCurrentPrefixURL(fieldConfig);
return prefix_url;
} @Override
//The unique identifier for this kind of configuration,
//and also the key for the $configs Map used in edit.vm
public String getObjectKey() {
return "PrefixUrlConfig";
} @Override
// Return the Object used in the Velocity edit context in $configs
public Object getConfigurationObject(Issue issue, FieldConfig fieldConfig) {
Map result = new HashMap();
result.put("prefixurl", DAO.getCurrentPrefixURL(fieldConfig));
return result;
} @Override
// Where the Edit link should redirect to when it's clicked on
public String getBaseEditUrl() {
return "EditPrefixUrlConfig.jspa";
}
}

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

 package com.mt.mcs.customfields.configurableurl;

 import com.atlassian.jira.issue.fields.config.FieldConfig;
import com.opensymphony.module.propertyset.PropertySet;
import com.opensymphony.module.propertyset.PropertySetManager;
import org.apache.log4j.Logger; import java.util.HashMap; public class DAO { public static final Logger log; static {
log = Logger.getLogger(DAO.class);
} private static PropertySet ofbizPs = null; private static final int ENTITY_ID = 20000; private static PropertySet getPS() {
if (ofbizPs == null) {
HashMap ofbizArgs = new HashMap();
ofbizArgs.put("delegator.name", "default");
ofbizArgs.put("entityName", "prefix_url_fields");
ofbizArgs.put("entityId", new Long(ENTITY_ID));
ofbizPs = PropertySetManager.getInstance("ofbiz", ofbizArgs);
}
return ofbizPs;
} private static String getEntityName(FieldConfig fieldConfig) {
Long context = fieldConfig.getId();
String psEntityName = fieldConfig.getCustomField().getId() + "_" + context + "_config";
return psEntityName;
} public static String retrieveStoredValue(FieldConfig fieldConfig) {
String entityName = getEntityName(fieldConfig);
return getPS().getString(entityName);
} public static void updateStoredValue(FieldConfig fieldConfig, String value) {
String entityName = getEntityName(fieldConfig);
getPS().setString(entityName, value);
} public static String getCurrentPrefixURL(FieldConfig fieldConfig) {
String prefixurl = retrieveStoredValue(fieldConfig);
log.info("Current stored prefix url is " + prefixurl);
if (prefixurl == null || prefixurl.equals("")) {
prefixurl = null;
}
return prefixurl;
}
}

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

 package com.mt.mcs.customfields.configurableurl;

 import com.atlassian.jira.issue.Issue;
import com.atlassian.jira.issue.customfields.impl.FieldValidationException;
import com.atlassian.jira.issue.customfields.impl.GenericTextCFType;
import com.atlassian.jira.issue.customfields.manager.GenericConfigManager;
import com.atlassian.jira.issue.customfields.persistence.CustomFieldValuePersister;
import com.atlassian.jira.issue.fields.CustomField;
import com.atlassian.jira.issue.fields.config.FieldConfig;
import com.atlassian.jira.issue.fields.config.FieldConfigItemType;
import com.atlassian.jira.issue.fields.layout.field.FieldLayoutItem; import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern; public class PrefixUrlCFType extends GenericTextCFType { public PrefixUrlCFType(CustomFieldValuePersister customFieldValuePersister, GenericConfigManager genericConfigManager) {
super(customFieldValuePersister, genericConfigManager);
} @Override
public List<FieldConfigItemType> getConfigurationItemTypes() {
final List<FieldConfigItemType> configurationItemTypes = super.getConfigurationItemTypes();
configurationItemTypes.add(new PrefixURLConfigItem());
return configurationItemTypes;
} @Override
public Map<String, Object> getVelocityParameters(final Issue issue,
final CustomField field,
final FieldLayoutItem fieldLayoutItem) {
final Map<String, Object> map = super.getVelocityParameters(issue, field, fieldLayoutItem); // This method is also called to get the default value, in
// which case issue is null so we can't use it to add currencyLocale
if (issue == null) {
return map;
} FieldConfig fieldConfig = field.getRelevantConfig(issue);
//add what you need to the map here
map.put("currentPrefixURL", DAO.getCurrentPrefixURL(fieldConfig)); return map;
} public String getSingularObjectFromString(final String string) throws FieldValidationException
{
// JRA-14998 - trim the value.
final String value = (string == null) ? "Default" : string.trim();
if (value != null && value != "Default") {
Pattern p = Pattern.compile("^[0-9A-Za-z]+$");
Matcher m = p.matcher(value);
if (!m.matches()) {
throw new FieldValidationException("Not Valid, only support a-z, A-Z and 0-9 ...");
}
}
return value;
}
}

Velocity模板引擎

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

view.vm

 #disable_html_escaping()
#set($defaultValue = "Default")
#if ($value && $value != $defaultValue)
#if ($currentPrefixURL)
<a class="tinylink" target="_blank" href="$currentPrefixURL$value">$!textutils.htmlEncode($value)</a>
#else
#set($displayValue = "没有配置URL前缀...")
$!textutils.htmlEncode($displayValue)
#end
#elseif ($value == $defaultValue)
#set($displayValue = "请输入相关信息...")
$textutils.htmlEncode($displayValue)
#else
#set($displayValue = "出现错误了....")
$textutils.htmlEncode($displayValue)
#end

edit.vm

 #disable_html_escaping()
#customControlHeader ($action $customField.id $customField.name $fieldLayoutItem.required $displayParameters $auiparams)
#set($configObj = $configs.get("PrefixUrlConfig"))
#set($prefixUrl = $configObj.get("prefixurl"))
#set($defaultValue = "Default")
#if ($value == $defaultValue)
<input class="text" id="displayText" name="displayText" type="text" value="" onchange="changeValue(${customField.id})">
<input class="text" id="$customField.id" name="$customField.id" type="hidden" value="$textutils.htmlEncode($!value)">
#else
<input class="text" id="$customField.id" name="$customField.id" type="text" value="$textutils.htmlEncode($!value)">
#end
<script type="text/javascript">
function changeValue(cfElmId) {
var cfElmId = cfElmId.id;
var element = document.getElementById("displayText");
var elmVal = element.value;
var cfElm = document.getElementById(cfElmId);
cfElm.value = elmVal;
}
</script>
#customControlFooter ($action $customField.id $fieldLayoutItem.fieldDescription $displayParameters $auiparams)

WebWork Action

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

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

 <webwork1 key="url-configurable"
name="URL configuration action"
class="java.lang.Object">
<description>
The action for editing a prefix url custom field type configuration.
</description>
<actions>
<action name="com.mt.mcs.customfields.configurableurl.EditPrefixUrlConfig"
alias="EditPrefixUrlConfig">
<view name="input">
/templates/com/mt/mcs/customfields/configurableurl/edit-config.vm
</view>
<view name="securitybreach">
/secure/views/securitybreach.jsp
</view>
</action>
</actions>
</webwork1>

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

 <html>
<head>
<title>
$i18n.getText('common.words.configure')
$action.getCustomField().getName()
</title>
<meta content="admin" name="decorator">
<link rel="stylesheet" type="text/css" media="print" href="/styles/combined-printtable.css">
<link rel="stylesheet" type="text/css" media="all" href="/styles/combined.css">
<style>
table.base-table {
margin: 15px auto;
border-spacing: 5px 10px;
line-height: 1.5;
font-size: 16px;
}
input.prefixurl {
outline: none;
box-shadow: bisque;
width: 350px !important;
}
table.base-table input#Save {
margin-left: 80px;
}
</style>
</head>
<body>
<h2 class="formtitle">
$i18n.getText('common.words.configure') $action.getCustomField().getName()
</h2>
<div class="aui-message aui-message-info">
<p class="title">
<span class="aui-icon icon-info"></span>
<strong>Notice</strong>
</p>
<p>
Config the prefix of your URL.
</p>
<p>
At the end of the URL, you need to add a '/', such as 'http://192.168.11.234/' !
</p>
</div>
<form action="EditPrefixUrlConfig.jspa" method="post" class="aui">
<table class="base-table">
<tr>
<td>
Prefix Url:&nbsp;
</td>
<td>
#set($prefix_url = $action.getPrefixurl())
<input type="text" name="prefixurl" id="prefixurl" value="$!prefix_url" class="text prefixurl">
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit" name="Save" id="Save" value="$i18n.getText('common.words.save')" class="aui-button">
<a href="ConfigureCustomField!default.jspa?customFieldId=$action.getCustomField().getIdAsLong().toString()"
id="cancelButton" class="aui-button" name="ViewCustomFields.jspa">
Cancel
</a>
</td>
</tr>
</table>
<input type="hidden" name="fieldConfigId" value="$fieldConfigId">
</form>
</body>
</html>

Action Class

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

 package com.mt.mcs.customfields.configurableurl;

 import com.atlassian.jira.config.managedconfiguration.ManagedConfigurationItemService;
import com.atlassian.jira.issue.customfields.impl.FieldValidationException;
import com.atlassian.jira.security.Permissions;
import com.atlassian.jira.web.action.admin.customfields.AbstractEditConfigurationItemAction;
import com.opensymphony.util.UrlUtils; public class EditPrefixUrlConfig extends AbstractEditConfigurationItemAction { protected EditPrefixUrlConfig(ManagedConfigurationItemService managedConfigurationItemService) {
super(managedConfigurationItemService);
} private String prefixurl; public void setPrefixurl(String prefixurl) {
this.prefixurl = prefixurl;
} public String getPrefixurl() {
return this.prefixurl;
} protected void doValidation() {
String prefix_url = getPrefixurl();
prefix_url = (prefix_url == null) ? null : prefix_url.trim();
if (prefix_url == null) {
return;
}
if (!UrlUtils.verifyHierachicalURI(prefix_url)) {
addErrorMessage("ERROR: " + prefix_url + " is not a valid URL...");
}
} protected String doExecute() throws Exception {
if (!isHasPermission(Permissions.ADMINISTER)) {
return "securitybreach";
}
if (getPrefixurl() == null) {
setPrefixurl(DAO.retrieveStoredValue(getFieldConfig()));
}
DAO.updateStoredValue(getFieldConfig(), getPrefixurl());
String save = request.getParameter("Save");
if (save != null && save.equals("Save")) {
setReturnUrl("/secure/admin/ConfigureCustomField!default.jspa?customFieldId=" + getFieldConfig().getCustomField().getIdAsLong().toString());
return getRedirect("not used");
}
return INPUT;
}
}

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

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

JIRA Plugin Development——Configurable Custom Field Plugin的更多相关文章

  1. Tips For Your Maya Plugin Development

    (The reason why I write English blog is that I'm trying to improve my written English. The Chinese v ...

  2. eclipse preference plugin development store and get

    eclipse plugin development: E:\workspaces\Eclipse_workspace_rcp\.metadata\.plugins\org.eclipse.pde.c ...

  3. redmine computed custom field formula tips

    项目中要用到Computed custom field插件,公式不知道怎么写,查了些资料,记录在这里. 1.http://apidock.com/ruby/Time/strftime 查看ruby的字 ...

  4. [转]How to query posts filtered by custom field values

    Description It is often necessary to query the database for a list of posts based on a custom field ...

  5. ActiveMQ(5.10.0) - Building a custom security plug-in

    If none of any built-in security mechanisms works for you, you can always build your own. Though the ...

  6. Ionic2 调用Custom Cordova Plugin方法

    APP升级到Ionic2之后,如何调用自己写的pulgin,一直测试不成功,现记录这一经过. plugin目前可以分为3类,A类是ionic-native自带的,可以直接导入Typescript类,直 ...

  7. eclipse plugin development -menu

    org.eclipse.ui.menus locationURI MenuContribution locationURI = "[Scheme]:[id]?[argument-list]& ...

  8. SharePoint Development - Custom Field using Visual Studio 2010 based SharePoint 2010

    博客地址 http://blog.csdn.net/foxdave 自定义列表的时候有时候需要自定义一些字段来更好地实现列表的功能,本文讲述自定义字段的一般步骤 打开Visual Studio,我们还 ...

  9. 给萌新的 TS custom transformer plugin 教程——TypeScript 自定义转换器插件

    xuld/原创 Custom transformer (自定义转换器)是干什么的 简单说,TypeScript 可以将 TS 源码编译成 JS 代码,自定义转换器插件则可以让你定制生成的代码.比如删掉 ...

随机推荐

  1. 如何配置使用Dnsmasq

    此文已由作者赵斌授权网易云社区发布 欢迎访问网易云社区,了解更多网易技术产品运营经验. 一.前言 最近为了测试内容分发网络(Content Delivery Network,简称 CDN)CDN在调用 ...

  2. bzoj 3722: PA2014 Final Budowa

    3722: PA2014 Final Budowa Time Limit: 1 Sec  Memory Limit: 128 MBSubmit: 303  Solved: 108[Submit][St ...

  3. NoSuchMethodError idea解决jar包冲突

    报NoSuchMethodError(使用spring boot框架idea)一般是jar包冲突 Exception in thread"main" java.lang.NoSuc ...

  4. [USACO14MAR]破坏Sabotage 二分答案

    题目描述 Farmer John's arch-nemesis, Farmer Paul, has decided to sabotage Farmer John's milking equipmen ...

  5. Collections.copy

    List<String> names = Arrays.asList(new String[nameList.size()]); Collections.copy(names, nameL ...

  6. AJAX (分页)

    <!-- 企业新闻列表开始,图尺寸550*310,如果没图,则在li上加on --> <div class="common-box new-box"> &l ...

  7. Silverlight 创建 ImageButton

    这几天一直在折腾怎么在silverlight 按钮上添加图片,直接向imagebutton那样设置成属性可以直接更改,最后到处查找资料终于搞出一个imagebutton了. <Style x:K ...

  8. CodeForces - 95B

    Petya loves lucky numbers. Everybody knows that positive integers are lucky if their decimal represe ...

  9. Mac开启自带的Apache服务器

    OSX版本10.13.6 1.开启 sudo apachectl start 2.关闭 sudo apachectl stop 3.重启 sudo apachectl restart 默认的Apach ...

  10. PHP拾贝

    $_SERVER['DOCUMENT_ROOT']指向了web服务器文档树的根.(E:/wamp/www/) ********************************************* ...