关于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,具体可参考:
创建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:
</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,讲解非常详细;