JIRA-开发秘籍-全-

JIRA 开发秘籍(全)

原文:annas-archive.org/md5/ec9789c2235a7f69f0f123e487a86233

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书是掌握 JIRA 扩展与定制的全方位资源。你将学习如何创建自己的 JIRA 插件,定制 JIRA UI 的外观和体验,处理工作流、问题、自定义字段等,功能丰富。

本书首先介绍了简化插件开发过程的配方,接着有一整章专门讲解 JIRA 插件框架,帮助你掌握 JIRA 中的插件。

接下来,我们将学习编写自定义字段插件,创建新的字段类型或自定义搜索器。然后,我们将学习如何编程和定制工作流,将 JIRA 转变为一个更加用户友好的系统。

然后,我们将讨论如何定制 JIRA 的各种搜索功能,例如 JQL、插件中的搜索、管理过滤器等等。

然后,本书转向编程问题;即创建/编辑/删除问题,创建新的问题操作,使用 JIRA API 管理问题上的其他各种操作等等。

在本书的后半部分,你将学习如何通过添加新的标签页、菜单和 Web 项来定制 JIRA,使用 REST、SOAP 或 XML/RPC 接口与 JIRA 通信,并与 JIRA 数据库进行交互。

本书最后一章是关于有用的 JIRA 常见配方。

本书内容

第一章,插件开发流程,介绍了 JIRA 插件开发过程的基本知识。详细讲解了开发环境的设置、插件的创建、部署和测试等内容。

第二章,理解插件框架,详细讲解了 JIRA 的架构,并介绍了各种插件点。它还介绍了如何从源代码构建 JIRA,并扩展或重写现有的 JIRA 功能。

第三章,使用自定义字段,介绍了如何通过编程在 JIRA 中创建自定义字段、编写自定义字段搜索器以及与自定义字段相关的各种有用配方。

第四章,编程工作流,介绍了编程 JIRA 工作流的各种方法。包括编写新的条件、验证器、后续功能等,并包含与扩展工作流相关的有用配方。

第五章,JIRA 中的小工具与报告,涵盖了 JIRA 的报告功能。详细介绍了编写报告、仪表盘小工具等。

第六章,JIRA 搜索的强大功能,讲解了 JIRA 的搜索功能及如何通过 JIRA API 扩展搜索功能。

第七章,编程问题,介绍了用于编程管理问题的各种 API 和方法。涵盖了 CRUD 操作、处理附件、编程变更日志和问题链接、时间跟踪等内容。

第八章,自定义 UI,介绍了扩展和修改 JIRA 用户界面的各种方法。

第九章,远程访问 JIRA,介绍了 JIRA 的远程功能——REST、SOAP 和 XML/RPC,并探讨了扩展这些功能的方法。

第十章,处理数据库,介绍了 JIRA 的数据库架构,并详细讨论了主要的表格。还探讨了扩展存储、访问或修改数据的不同方式。

第十一章,有用的实例,涵盖了一些有用的实例,这些实例不属于前面章节的内容,但足够强大,值得关注!赶紧阅读吧!!

本书所需的内容

本书侧重于 JIRA 开发。最低要求你需要以下软件:

  • JIRA 4.x+

  • JAVA 1.6+

  • Maven 2.x

  • Atlassian 插件 SDK

  • 选择你喜欢的 IDE。本书中的示例使用了 Eclipse 和 SQL Developer。

有些实例过于简单,不需要使用完整的插件开发流程,书中会在相关部分特别指出!

本书适合谁阅读

如果你是 JIRA 开发者或项目经理,想要充分利用 JIRA 的精彩功能,那么这本书非常适合你。

约定

本书中有多种文本风格,用于区分不同类型的信息。以下是这些风格的一些示例,以及它们的含义解释。

文本中的代码词汇如下所示:“字段oldvaluenewvalue通过方法getChangelogValue填充。”

代码块如下所示:

<!-- entity to represent a single change to an issue. Always part of a change group -->
    <entity entity-name="ChangeItem" table-name="changeitem" package-
     name="">
        <field name="id" type="numeric"/>
        <field name="group" col-name="groupid" type="numeric"/>
        <!—relations and indexes -->
    </entity>

当我们希望引起你对代码块中特定部分的注意时,相关行或项目会以粗体显示:

<!-- entity to represent a single change to an issue. Always part of a change group -->
    <entity entity-name="ChangeItem" table-name="changeitem" package-
     name="">
 <field name="oldvalue" type="extremely-long"/>
 <!-- a string representation of the new value (i.e. 
 "Documentation" instead of "4" for a component which might be 
 deleted) -->
        <!—relations and indexes -->
    </entity>

任何命令行输入或输出如下所示:

maven war:webapp

新术语重要词汇以粗体显示。你在屏幕上、菜单或对话框中看到的词汇,通常会这样显示:“你一定注意到了新的查看问题页面。”

注意

警告或重要说明会以这样的框框显示。

提示

提示和技巧通常是这样的形式。

读者反馈

我们始终欢迎读者反馈。告诉我们你对这本书的看法——你喜欢或可能不喜欢的部分。读者反馈对我们开发出你真正能够从中获得最大收益的书籍非常重要。

要给我们发送一般反馈,只需通过电子邮件发送至<feedback@packtpub.com>,并在邮件主题中注明书名。

如果您有需要的书籍,并希望我们出版, 请通过www.packtpub.com上的建议书名表单,或者通过电子邮件<suggest@packtpub.com>告诉我们。

如果您在某个领域有专业知识并且有兴趣撰写或贡献书籍内容,请查看我们在www.packtpub.com/authors上的作者指南。

客户支持

既然您已经是 Packt 书籍的骄傲拥有者,我们提供了一些帮助,帮助您最大限度地利用您的购买。

下载本书的示例代码

您可以从您的账户下载所有购买的 Packt 书籍的示例代码,网址是www.PacktPub.com。如果您在其他地方购买了本书,您可以访问www.PacktPub.com/support并注册,以便直接通过电子邮件将文件发送给您。

勘误

尽管我们已经尽力确保内容的准确性,但错误仍然会发生。如果您在我们的书籍中发现错误——可能是文本或代码中的错误——我们将非常感激您能报告给我们。通过这样做,您可以避免其他读者的困扰,并帮助我们改进后续版本的书籍。如果您发现任何勘误,请访问www.packtpub.com/support报告,选择您的书籍,点击勘误提交表单链接,填写您的勘误详情。一旦您的勘误被验证,您的提交将被接受,并且勘误将被上传到我们的网站,或添加到该书标题下的任何现有勘误列表中。您可以通过选择您的书籍标题来查看任何现有勘误,网址是www.packtpub.com/support

盗版

互联网上的版权材料盗版是所有媒体面临的持续问题。在 Packt,我们非常重视版权和许可证的保护。如果您在互联网上遇到我们作品的任何非法复制品,请立即向我们提供地址或网站名称,以便我们采取相应措施。

如果您发现涉嫌盗版的材料,请通过<copyright@packtpub.com>与我们联系,并提供链接。

我们感谢您在保护我们作者方面的帮助,以及我们为您提供有价值内容的能力。

问题

如果您在书籍的任何部分遇到问题,可以通过<questions@packtpub.com>联系我们,我们将尽力解决。

第一章:插件开发流程

本章内容包括:

  • 设置开发环境

  • 创建骨架插件

  • 部署 JIRA 插件

  • 测试和调试

介绍

如我们所知,Atlassian JIRA 主要是一个问题跟踪项目跟踪系统。然而,很多人并不知道的是,它强大的定制功能使得我们能够将其转变成完全不同的系统!比如,帮助台系统、用户故事管理系统、在线审批流程等。这些都可以在 JIRA 本身作为问题跟踪和项目跟踪功能的基础上实现,而 JIRA 在这一领域无疑是市场上的最佳选择。

那么,这些定制化功能是什么呢?我们如何将我们所知道的 JIRA 转变成我们需要的产品?或者只是为我们的组织增加一些特定的功能?

这些问题的答案可能可以用一个词来概括——插件。JIRA 赋予了用户编写插件的能力,从而使他们能够按自己的需求定制功能。

但是,这就是唯一的方法吗?当然不是!JIRA 本身通过其用户界面提供了许多定制选项,在更复杂的情况下,你可以通过像 jira-application.properties 这样的属性文件进行配置。在某些情况下,你还可能需要修改一些 JIRA 核心文件,以调整功能或解决问题。我们将在接下来的章节中看到更多内容,但插件是进入 JIRA 定制化的最佳切入点。我们将在这本教程中从这里开始,然后再深入探讨更详细的内容。

什么是 JIRA 插件?

那么,什么是 JIRA 插件呢?JIRA 本身是一个用 Java 编写的 Web 应用程序。但这并不意味着你需要知道 Java 来编写插件,尽管在大多数情况下你可能需要。你可能只需要编写一个简单的描述符文件,来添加一些链接。如果这样让你这个非 Java 开发者感到满意,那就看看 JIRA 支持的各种插件模块吧。

JIRA 插件是一个 JAR 文件,包含一个必需的插件描述符以及一些可选的 Java 类和 Velocity 模板。Velocity 模板用于渲染与插件关联的 HTML 页面,但在某些情况下,你也可能需要引入 JSP 来使用 JIRA 中一些现有的模板。与 Velocity 模板不同,JSP 无法嵌入插件中,而应该被放入 JIRA Web 应用程序中的适当文件夹中。

插件描述符是插件中唯一必需的部分,它是一个 XML 文件,必须命名为 atlassian-plugin.xml。此文件位于插件的根目录。atlassian-plugin.xml 文件定义了插件中的各种模块。可用的插件模块类型包括报告、自定义字段类型等,这些将在下一章中详细讨论。

插件开发流程

开发 JIRA 插件的过程可以根据我们尝试实现的功能而复杂度不同。插件开发过程本质上是一个四步过程:

  1. 开发插件。

  2. 将其部署到我们的本地 JIRA。

  3. 测试插件功能。

  4. 如有需要,修改并重新部署插件。

本书中的各种食谱会详细解释这些内容!

JIRA 在启动时会识别当前安装中所有已部署的插件。你可以部署多个插件,但有些事情需要注意!

atlassian-plugin.xml文件有一个插件,该键应该在所有插件中是唯一的。它类似于 Java 包。插件中的每个模块也有一个在插件内唯一的键。插件键和模块键组合在一起,并用冒号分隔,形成一个插件模块的完整键。

以下是一个没有任何插件模块的示例atlassian-plugin.xml文件:

<!-- the unique plugin key -->
<atlassian-plugin key="com.jtricks.demo" name="Demo Plugin" plugins-version="2">
    <!-- Plugin Info -->
    <plugin-info>
        <description>This is a Demo Description</description>
        <version>1.0</version>
        <!-- optional  vendor details -->
        <vendor name="J-Tricks" url="http://www.j-tricks.com"/>
    </plugin-info> 
    . . . 1 or more plugin modules . . .
</atlassian-plugin>

如你所见,插件包含了描述、版本、供应商信息等详细信息。

当一个插件被加载时,插件中的所有独立模块也会被加载。插件类会覆盖系统类,因此如果有一个动作与 JIRA 的动作具有相同的别名,那么将加载插件的动作类。我们将在接下来的章节中详细了解如何扩展动作。

假设你的插件中有一个报告模块,它将如下所示:

<report key="demo-report" name="My Demo Report" ....>
...
</report>

在这种情况下,插件键将是com.jtricks.demo,模块键将是com.jtricks.demo:demo-report

等一下,在你开始编写你梦寐以求功能的插件之前,先看看 Atlassian 插件交换平台,看看是否有人已经为你完成了这项工作!

Atlassian 插件交换平台

Atlassian 插件交换平台是一个一站式商店,您可以在这里找到世界各地的人编写的所有商业和开源插件的完整列表。有关更多详情,请参见plugins.atlassian.com/search/by/jira

故障排除

在部署插件时,常见的场景是即使一切看起来正常,插件仍然无法加载。确保插件的键值是唯一的,并且没有与您或其他第三方的插件重复!

同样适用于单独的插件模块。

设置开发环境

现在我们已经了解了插件是什么,接下来让我们开始编写一个插件!编写 JIRA 插件的第一步是设置开发环境,如果你还没有完成的话。在这个食谱中,我们将看到如何设置本地环境。

为了简化插件开发,Atlassian 提供了Atlassian 插件软件开发工具包SDK)。它包含 Maven 和一个预配置的settings.xml,让事情变得更加简便。

Atlassian 插件 SDK 可用于开发其他 Atlassian 产品的插件,包括 Confluence、Crowd 等,但我们仅专注于 JIRA。

准备就绪

以下是运行 Atlassian 插件 SDK 的前提条件:

  • SDK 的默认端口 2990 应该是可用的。这一点很重要,因为不同的端口为不同的 Atlassian 产品保留。

  • 必须安装 JDK Java 版本 1.5 - 6。

  • 确保正确设置 JAVA_HOME,并且命令 java –version 输出正确的 Java 版本信息。

  • 当然,你的开发环境中应该已经安装了 JIRA 4.x 或更高版本。

    注意

    确保为你的 JIRA 使用一个上下文路径,因为已知 SDK 在上下文路径为空时无法正常工作。更多细节请参见studio.atlassian.com/browse/AMPS-122

如何操作…

  1. 一旦我们安装了 Java 并准备好端口,就可以从maven.atlassian.com/content/repositories/atlassian-public/com/atlassian/amps/atlassian-plugin-sdk/下载最新版本的 Atlassian Plugin SDK。

  2. 将该版本解压到你选择的目录中。接下来我们将该目录称为 SDK_HOME

  3. 将 SDK 的 bin 目录添加到环境 PATH 变量中。

  4. 创建一个新的环境变量 M2_HOME,指向 SDK Home 中的 Apache-Maven 目录。

  5. SDK 中嵌入的仓库文件夹中已经包含了许多常用的依赖项。要使用这些依赖项,请编辑 M2_HOME/conf/ 下的 settings.xml,并修改 localRepository 属性,将其指向嵌入的仓库文件夹。默认情况下,它将使用 USER_HOME/.m2/repository

  6. 安装你选择的 IDE。Atlassian 推荐使用 Eclipse、IntelliJ IDEA 或 NetBeans,它们都支持 Maven。

  7. 准备、设置、开始…

工作原理…

只要正确执行这些步骤,我们就可以为 JIRA 插件创建一个开发环境。

下一步是创建一个骨架插件,将其导入到你的 IDE 中,并开始编写一些代码!创建骨架插件、部署插件等操作将在以下教程中详细说明。

还有更多…

尽管上述步骤在大多数情况下都能奏效,但我们也会遇到一些设置开发环境并不那么简单的情况。例如,如果机器在防火墙后面,Maven 可能需要额外的设置。你可能已经安装了本地的 Maven 版本。在这一节中,我们将看到一些在类似情况下有用的提示。

Maven 的代理设置

如果你位于防火墙后面,请确保在 Maven 的 settings.xml 文件中配置代理。代理可以按如下方式配置:

<settings>
  .
  <proxies>
   <proxy>
      <active>true</active>
      <protocol>http</protocol>
      <host>proxy.demo.com</host>
      <port>8080</port>
      <username>demouser</username>
      <password>demopassword</password>
      <nonProxyHosts>localhost|*.demosite.com</nonProxyHosts>
    </proxy>
  </proxies>
  .
</settings>

了解更多有关 Maven 的信息,参见maven.apache.org/index.html

使用本地 Maven

如果你是开发者,在许多情况下,你的本地机器上已经安装了 Maven。在这种情况下,指向你本地的 Maven,并更新相应的settings.xml,将 Atlassian 插件 SDK 附带的默认 settings.xml 中的仓库详情添加进去。

配置 IDE 使用 SDK

如果你使用的是 IntelliJ IDEA,那会非常简单,因为 IDEA 开箱即集成了 Maven。只需选择 pom.xml 加载项目即可!

如果你使用的是 Eclipse,请确保安装了 M2Eclipse 插件。这是因为 Eclipse 通过 Sonatype M2Eclipse 插件集成了 Maven。你可以在 confluence.atlassian.com/display/DEVNET/Configuring+Eclipse+to+use+the+SDK 查找有关如何配置的更多细节。

故障排除

如果你看到 Maven 下载错误,如无法解析工件,请确保验证以下内容:

  • Maven settings.xml 中的条目正确。即,它指向正确的仓库

  • 如果需要,进行代理配置

  • 如果上述方法都无效,本地机器上的防病毒软件会被禁用!真的,它会有所不同。

另见

  • 创建骨架插件

创建骨架插件

在本教程中,我们将创建一个骨架插件。我们将使用 Atlassian Plugin SDK 来创建这个骨架!

准备工作

确保你已经安装了 Atlassian Plugin SDK,并且本地机器上运行了 JIRA 4.x 版本。

如何操作...

  1. 打开命令窗口,进入你想创建插件的文件夹。

    注意

    确保你使用的目录中没有空格,因为已知 SDK 在包含空格的目录中无法正常工作。详情请参见 studio.atlassian.com/browse/AMPS-126

  2. 输入atlas-create-jira-plugin并按Enter

  3. 在提示时输入groupID。GroupID 通常来自于你的组织名称,并且通常类似于 Java 包名。当然,如果你想保持它的独立性,可以在后续过程中输入不同的包名。GroupID 将与artifactId一起用于标识你的插件。

    例如:com.jtricks.demo

  4. 输入artifactId—该工件的标识符。这里不要使用空格。

    例如:demoplugin

  5. Version—默认版本是1.0-SNAPSHOT。如果你想更改它,请输入新版本,或按Enter 保持默认版本。

    例如:1.0

  6. Package—如果包名与groupID相同,请按Enter。如果不同,请在此输入新值并按Enter

    例如,com.jtricks.mypackage

  7. 在提示时确认选择。如果你想更改任何已输入的值,请输入N并按Enter

  8. 等待BUILD SUCCESSFUL消息。你可能会看到一些可以忽略的警告。

它是如何工作的...

插件骨架就是一组目录和子目录,带有pom.xml(Maven 项目对象模型)文件以及适当文件夹中的一些示例 Java 和 XML 文件。

这里是项目在 Eclipse 中的外观快照。它还显示了默认atlassian-plugin.xml文件的设计视图:

它是如何工作的...

如你所见,根目录下有一个pom.xml文件和一个src文件夹。根目录下还为你创建了一个示例的LICENSE文件和一个README文件。

src文件夹下,你会发现两个文件夹,maintest,它们的文件夹结构相同。所有的主 Java 代码都放在main文件夹下。你编写的任何 JUnit 测试都会放在test文件夹下的相同位置。测试文件夹下还有一个额外的文件夹,it,所有的集成测试都将放在那里!

你会在src/main/resources下找到插件描述文件,文件中已填充了示例值。前面截图中的值是从pom.xml文件中填充的。在我们的例子中,当插件构建时,插件密钥将填充为com.jtricks.demo:demoplugin

src/test下还有两个文件夹,src/test/resources,它们将包含单元测试或集成测试所需的任何资源,src/test/xml文件夹可以存放来自其他 JIRA 实例的 XML 数据。如果提供了 XML 文件,SDK 将在运行集成测试之前使用它来配置 JIRA 实例。

这就是我们的插件骨架。剩下的就是一些有用的 Java 代码和atlassian-plugin.xml文件中的正确模块类型!

注意

请记住,第一次运行 Maven 会花费一些时间,因为它会将所有依赖项下载到本地仓库。一个咖啡休息时间可能都不够!如果你可以选择,最好计划好你的餐饮时间。 😉

还有更多...

有时,对于极客来说,运行单个命令来创建一个项目要比逐步创建更简单。在本节中,我们将快速了解如何实现。我们还将看看如何在选择不安装 m2eclipse 的情况下创建一个 Eclipse 项目。

朝着插件骨架迈出一步

你可以通过将groupIDartifactId等参数作为参数传递给atlas-create-jira-plugin命令来忽略交互模式。

atlas-create-jira-plugin -g my_groupID -a my_artefactId -v my_version -p my_package –non-interactive

在这个例子中,对于我们之前看到的值,单行命令将是:

atlas-create-jira-plugin -g com.jtricks.demo -a demoplugin -v 1.0 -p com.jtricks.mypackage –non-interactive

你可以选择并选择参数,也可以在交互模式下提供其余参数!

创建 Eclipse 项目

如果你没有使用 m2eclipse,只需从包含pom.xml文件的文件夹中运行以下命令:

atlas-mvn eclipse:eclipse

这将为 Eclipse 生成插件项目,你可以将该项目导入到 IDE 中。

如果你想清理旧项目并重新创建,请键入atlas-mvn eclipse:clean eclipse:eclipse

使用 IDEA 或 m2eclipse,只需打开一个文件即可。也就是说,你可以通过选择 文件 | 导入 | 现有 Maven 项目 来导入项目,并选择相关项目。

另见

  • 部署插件

  • 修改并重新部署插件

部署插件

在本教程中,我们将学习如何将插件部署到 JIRA。我们将看到使用 Atlassian 插件 SDK 进行自动化部署和手动部署的过程。

准备工作

确保你已经设置好开发环境,正如我们之前讨论的那样。同时,骨架插件现在应该已经实现了插件逻辑。

如何操作...

使用 Atlassian 插件 SDK 安装 JIRA 插件简直易如反掌。下面是操作方法:

  1. 打开命令窗口,进入插件的根文件夹,也就是 pom.xml 所在的文件夹。

  2. 输入 atlas-run 并按 Enter。你也可以传递更多选项作为该命令的参数,详细信息可以在此查看:confluence.atlassian.com/display/DEVNET/atlas-run

  3. 当 Maven 下载所有依赖库到本地仓库时,你会看到很多操作。像往常一样,第一次运行时会花费大量时间。

  4. 如果你在 Windows 上,如果弹出安全警告,点击 解除阻止 以允许传入的网络连接。

  5. 安装完成后,你将看到以下消息:

    [WARNING] [talledLocalContainer] INFO: Server startup in 123558 ms
    [INFO] [talledLocalContainer] Tomcat 6.x started on port [2990]
    [INFO] jira started successfully and available at http://localhost:2990/jira
    [INFO] Type CTRL-C to exit
    
  6. 在浏览器中打开 http://localhost:2990/jira

  7. 使用用户名 admin 和密码 admin 登录。

  8. 测试你的插件!你可以随时访问 管理 | 插件 菜单,确认插件是否已正确部署。

如果你已经安装了本地 JIRA,或者因为某些原因需要手动安装插件,你只需打包插件 JAR 文件并将其复制到 JIRA_Home/plugins/installed-plugins 目录中。

你可以使用以下命令打包插件:

atlas-mvn clean package

如果你还想将插件包安装到本地仓库,可以使用 atlas-mvn clean install 命令。

它是如何工作的...

有一个单一的命令完成整个过程:atlas-run。当你执行此命令时,它会做以下几件事:

  1. 构建你的插件 JAR 文件

  2. 如果这是你第一次运行该命令,它将下载最新/指定版本的 JIRA 到本地机器。

  3. 在你的插件/target 文件夹下创建一个虚拟的 JIRA 安装。

  4. 将 JAR 文件复制到 /target/jira/home/plugins/installed-plugins` 目录

  5. 在 Tomcat 容器中启动 JIRA。

现在,如果你查看目标文件夹,你会看到许多新创建的文件夹,这些是虚拟 JIRA 安装所创建的!两个主要文件夹分别是 container 文件夹,它包含 Tomcat 容器设置,以及 jira 文件夹,它包含 JIRA WAR 文件和 JIRA 配置!

你会在/target/jira/home下找到数据库(HSQLDB)、索引、备份和附件。并且你会看到你的jira-webapp位于/target/container/tomcat6x/cargo-jira-home/webapps/jira

如果你有任何需要放入 webapp 下的 JSP 文件,你必须将其复制到上述路径下的适当文件夹!

还有更多内容...

还有更多内容。

使用特定版本的 JIRA

如前所述,atlas-run部署的是最新版本的 JIRA。但如果你想将插件部署到早期版本的 JIRA 并进行测试怎么办?

有两种方法可以做到:

  1. atlas-run中作为参数提及 JIRA 版本;如果你已经部署了最新版本,确保运行atlas-clean

    • 运行atlas-clean(如有需要)。

    • 如果你正在为 JIRA 版本 4.1.2 开发,运行atlas-run –v 4.1.2atlas-run –version 4.1.2。将版本号替换为你选择的版本。

  2. 永久更改插件 pom.xml 中的 JIRA 版本:

    • 转到你的pom.xml

    • jira.version属性值修改为所需的版本。

    • jira.data.version修改为匹配的版本。

对于 JIRA 4.1.2,它将是这样的:

<properties>
    <jira.version>4.1.2</jira.version>
    <jira.data.version>4.1</jira.data.version>
</properties>

在每次运行时重用配置

假设你向虚拟 JIRA 添加了一些数据,如何在下次清理启动 JIRA 时保留这些数据?

这时一个新的 SDK 命令来帮助我们。

atlas-run完成后,也就是按下Ctrl + C之后,执行以下命令:

atlas-create-home-zip

这将在目标文件夹下生成一个名为generated-test-resources.zip的文件。将该文件复制到/src/test/resources文件夹或任何其他已知位置。现在修改pom.xml,在maven-jira-plugin的配置项下添加以下条目:

<productDataPath>${basedir}/src/test/resources/generated-test-resources.zip</productDataPath>

根据需要修改路径。下次运行atlas-run时将重用这些配置。

故障排除

  • 缺少 JAR 文件异常?确保settings.xml文件中的 local-repository 属性指向与 SDK 一起提供的嵌入式 Maven 仓库。如果问题仍然存在,手动下载缺失的 JAR 文件并使用atlas-mvn install将它们安装到本地仓库。

    留意可能会阻止下载的代理设置或防病毒设置!

  • BeanCreationException?确保你的插件是版本 2。检查你的atlassian-plugin.xml文件,看看是否包含以下条目。如果没有,请添加该条目:

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

完成后,运行atlas-clean,然后运行atlas-run

进行更改并重新部署插件

现在我们已经部署了测试插件,是时候添加一些适当的逻辑、重新部署插件并进行测试了。进行更改并重新部署插件非常简单。在这个食谱中,我们将快速查看如何做到这一点。

如何操作...

在 JIRA 应用程序仍在运行时,你可以对插件进行更改并重新部署。我们是这样做的:

  1. 保持 JIRA 应用程序在我们运行atlas-run的窗口中继续运行。

  2. 打开一个新的命令窗口,进入你的插件根目录,那里应该有 pom.xml 文件。

  3. 运行 atlas-cli

  4. 等待命令——等待消息

  5. 运行pi。Pi 代表“插件安装”,这将编译你的更改,打包插件 JAR,并将其安装到installed-plugins文件夹中。

现在,有一件事你需要留意!并不是所有插件模块都可以像这样在 JIRA 4.4 之前重新部署。以下是 JIRA 4.0.x 中可以通过 pi 重新加载的插件模块列表:

  • 组件导入

  • 小部件

  • 模块类型

  • 资源

  • REST

  • Servlet 上下文监听器

  • Servlet 上下文参数

  • Servlet 过滤器

  • Servlet

  • Web 项

  • Web 资源

  • Web 部分

如果你的插件模块不在上面的列表中,或者更改似乎没有反映出来,在运行 atlas-run 的命令窗口中按 Ctrl + C,然后重新运行 atlas-run 命令。这将重新部署插件并重启 JIRA。

在 JIRA 4.1 之后,SDK 支持重新加载更多模块,但是否有效取决于模块内部的实现。

JIRA 4.4+ 支持重新加载所有插件模块。

在 Eclipse 中调试

也可以以调试模式运行插件,并将 IDE 的远程调试器指向它。

以下是在 Eclipse 中执行此操作的步骤:

  1. 使用atlas-debug代替atlas-run

  2. 一旦虚拟 JIRA 启动并成功部署了你的插件,进入 Eclipse 中的 运行 | 调试配置

  3. 创建一个新的 远程 Java 应用程序

  4. 给定一个名称,保持默认设置,并将端口号设为 5005。这是虚拟 JIRA 运行的默认调试端口。

  5. 调试愉快!

另见

  • 设置开发环境

  • 创建骨架插件

测试和调试

测试驱动开发TDD)的世界里,编写测试是开发过程中的一部分。我不想让你厌烦为什么测试很重要!我们只要说,所有这些也适用于 JIRA 插件开发。

在本篇教程中,我们将看到运行 JIRA 插件的单元测试和集成测试的各种命令。

正在准备中

确保你已经设置了插件开发环境并创建了骨架插件!

你可能已经注意到,在 src/test/java/your_package/src/test/java/it 文件夹下创建了两个示例测试文件,分别用于单元测试和集成测试。

一旦准备好,就该编写一些测试并运行这些测试,确保一切按预期工作!

如何执行……

第一步是编写一些测试!我们推荐你使用一些强大的测试框架,比如 JUnit,并与 PowerMockMockito 等模拟框架配合使用。确保你在 pom.xml 中添加了有效的依赖项。

现在让我们做一个大胆的假设,你已经编写了几个测试!

以下是从命令行运行单元测试的命令:

atlas-unit-test

正常的 Maven 命令 atlas-mvn clean test 也做同样的事情。如果你正在运行集成测试,使用的命令是:

atlas-integration-test

或者使用 Maven 命令:atlas-mvn clean integration-test

一旦进入运行测试的阶段,我们有时会看到测试失败。此时需要调试。查看在 target/surefire-reports/ 下创建的 *.txt*.xml 文件,它们包含了所有执行的测试所需的详细信息。

现在,如果你想跳过各个阶段的测试,请使用 –skip-tests。例如,atlas-unit-test --skip-tests 将跳过单元测试。

你还可以直接使用 Maven 选项来跳过单元测试、集成测试或两者一起跳过。

  • -Dmaven.test.skip=true:跳过单元测试和集成测试

  • -Dmaven.test.unit.skip=true:跳过单元测试

  • -Dmaven.test.it.skip=true:跳过集成测试

它是如何工作的...

atlas-unit-test 命令仅在后台运行相关的 Maven 命令:atlas-mvn clean test,以执行各种单元测试。它还将输出生成到 surefire-reports 目录中以供参考或调试。

atlas-integration-test 做得更多。它在一个虚拟的 JIRA 环境中运行集成测试。它将启动一个运行在 Tomcat 容器中的新 JIRA 实例,并为该实例设置一些默认数据,包括一个持续三小时的临时许可证,并执行你的测试!

JIRA 如何区分单元测试和集成测试?这里文件夹结构起着重要作用。任何位于 src/test/java/it/ 文件夹下的内容将被视为集成测试,其余的将被视为单元测试!

还有更多...

还有更多内容。

使用自定义数据进行集成/功能测试

虽然 atlas-integration-test 通过在 JIRA 实例中设置一些默认数据让我们的生活更轻松,但我们可能还需要一些自定义数据来成功执行一些功能测试。

我们可以通过几个步骤来完成:

  1. 从预配置的 JIRA 实例中导出数据为 XML 格式。

  2. 将其放在 src/test/xml/ 目录下。

  3. 将此路径作为 localtest.propertiesjira.xml.data.location 属性的值,位于 src/main/resources 下。

XML 资源将在执行测试之前导入到 JIRA 中。

针对不同版本的 JIRA/Tomcat 进行测试

就像 atlas-run 命令一样,你可以使用 -v 选项将插件测试不同版本的 JIRA。如之前所述,如果你之前已对另一个版本进行了测试,请确保在运行测试前执行 atlas-clean

你还可以使用 -c 选项将其测试不同版本的 Tomcat 容器。

例如,atlas-clean && atlas-integration-test -v 3.0.1 -c tomcat5x 将使用 Tomcat 5 容器在 JIRA 版本 3.0.1 上测试你的插件。

另见

  • 设置开发环境

  • 部署插件

第二章:理解插件框架

在本章中,我们将详细了解 JIRA 架构和插件框架。我们还将看到以下几个部分:

  • 将插件从 v1 转换为 v2

  • 将资源添加到插件中

  • 将 Web 资源添加到插件中

  • 从源代码构建 JIRA

  • 将新的 Webwork 动作添加到 JIRA

  • 在 JIRA 中扩展 Webwork 动作

介绍

正如我们在前一章中看到的,JIRA 插件开发的过程可能比我们预期的要简单。借助 Atlassian Plugin SDK,开发人员可以将更多的时间用于关注插件逻辑,而不是繁琐的部署活动。毕竟,最终会影响到的是插件的逻辑!

本章详细介绍了 JIRA 各个组件如何融入 JIRA 架构,以及 JIRA 如何暴露各种可插拔的点。我们还将概述 JIRA 的系统插件,看看 JIRA 如何利用插件架构来为自己带来好处,接着介绍一些有用的技术。

JIRA 架构

我们将快速了解 JIRA 内部的各个组件是如何配合起来形成我们所知道的 JIRA 的。最好通过一个图表来描述,Atlassian 提供了一个简洁的图表,并附有详细的解释,可以在 confluence.atlassian.com/display/JIRA/JIRA+Architectural+Overview 查看。我们将稍微重新绘制该图,以简洁但有用的方式进行说明。

第三方组件

在深入了解 JIRA 架构之前,理解一些关键组件并熟悉它们可能是有用的。接下来,我们将概述 JIRA 的主要第三方依赖项。

注意

并非必须了解这些框架的所有细节,但如果你在插件开发过程中理解这些框架,将会非常有帮助。

Webwork

Webwork 仅仅是一个 Java Web 应用程序开发框架。以下是 Webwork 在 OpenSymphony 文档中的简要概述:

"它专门为开发者的生产力和代码简洁性而构建,提供了对构建可重用 UI 模板(例如表单控件、UI 主题、国际化、动态表单参数映射到 JavaBeans、强大的客户端和服务器端验证等)的强大支持。"

通过以下归档链接阅读更多关于 Webwork1 的信息: web.archive.org/web/20080328114803/http://www.opensymphony.com/webwork_old/src/docs/manual/

请注意,JIRA 使用的是 Webwork1 而不是 2。书中提到的所有 webwork 都是指 Webwork1 版本。JIRA 本身将该技术称为 webwork,但你会注意到文件、插件模块等地方,都会特别提到 webwork1,以强调其版本。

Seraph

Seraph 是 Atlassian 的开源网页认证框架。它提供了一个简单、可扩展的认证系统,JIRA 使用该系统进行所有认证工作。

了解更多关于 Seraph 的信息,请访问docs.atlassian.com/atlassian-seraph/latest/

OSUser

OSUser 是 OpenSymphony 的用户和组管理框架。它旨在为用户管理提供一个简单易用的 API。JIRA 在 4.3 版本之前使用了 OSUser 框架。

了解更多信息,请访问www.opensymphony.com/osuser/

JIRA 4.3+ 使用 Crowd 作为新的用户 API,详情请参阅docs.atlassian.com/atlassian-crowd/current/com/atlassian/crowd/embedded/api/CrowdService.html

PropertySet

PropertySet 是 OpenSymphony 的另一个开源框架,帮助您存储与任何具有唯一 ID 的“实体”相关的一组属性。这些属性将是键/值对,并且每次只能与一个实体关联。

了解更多内容,请访问www.opensymphony.com/propertyset/

OSWorkflow

OSWorkflow 是 OpenSymphony 团队的另一个开源框架。它是一个极为灵活的工作流实现,能够处理复杂的条件、验证器、后置功能等,以及许多其他功能。

了解更多关于 OSWorkflow 的信息,请访问www.opensymphony.com/osworkflow/

OfBiz 实体引擎

OfBiz 代表 Open For Business,而 OfBiz 实体引擎 是一组用于建模和管理实体特定数据的工具和模式。

根据关系数据库管理系统的标准实体关系建模概念,实体是由一组字段和一组与其他实体的关系定义的数据。

了解更多关于实体建模和概念的信息,请访问ofbiz.apache.org/docs/entity.html

Apache Lucene

以下是您可以在其文档中找到的 Apache Lucene 简单定义:

"Apache Lucene(TM) 是一个高性能、功能齐全的文本搜索引擎库,完全使用 Java 编写。它是一项适用于几乎所有需要全文搜索的应用程序的技术,尤其是跨平台应用。"

更多关于 Lucene 及其潜力的信息可以在lucene.apache.org/java/docs/index.html找到。

Atlassian Gadget JavaScript 框架

JIRA4 引入了强大的 Gadget 框架。Atlassian 在 Gadget 上实现了 OpenSocial,为了帮助开发者创建 Gadget,Atlassian 推出了 Gadgets JavaScript 框架,该框架封装了许多在 Gadget 之间常用的需求和功能。

关于小工具开发的更多信息可以参考 confluence.atlassian.com/display/GADGETDEV/Using+the+Atlassian+Gadgets+JavaScript+Framework

Quartz

Quartz 是一个开源的作业调度服务。它可以用来创建可以在任何 JAVA EE 和 SE 应用程序中调度的作业。这些任务被定义为标准的 Java 组件,调度器包含许多企业级功能,如 JTA 事务和集群。

阅读更多内容,请访问 www.quartz-scheduler.org/

架构解析…

最好通过图表来学习系统架构的复杂性。为了简要而有意义地解释 JIRA 架构,让我们快速浏览一下(或者长时间注视,看你喜欢哪种!)以下图表:

架构解析…

JIRA 是一个使用 MVC 架构构建的 Web 应用程序。它完全用 JAVA 编写,并作为 WAR 文件部署到 Java Servlet 容器中,如 Tomcat。

JIRA 核心功能的主要部分围绕着 JIRA 工具类和管理类,这些类成为 JIRA 的核心。但它也与许多第三方组件进行交互,如我们之前看到的,提供强大的功能,如工作流、权限、用户管理、搜索等。

与其他 Web 应用程序一样,我们从传入的请求开始。用户通过 Web 浏览器与 JIRA 交互。但也有其他方式与 JIRA 交互,比如使用 Jelly 脚本 或通过 RESTSOAPXML-RPC 进行远程调用。

认证和用户管理

无论请求通过何种方式发出,用户认证都在 JIRA 中通过 Seraph 完成,Seraph 是 Atlassian 提供的开源 Web 认证框架。Seraph 作为一个 Servlet 过滤器实现,它会拦截每一个传入请求,并将其与特定用户关联。它支持各种认证机制,如 HTTP 基本认证、基于表单的认证等,甚至在实施单点登录(SSO)时查找已经存储在用户会话中的凭证。

然而,Seraph 并不直接进行任何用户管理。它将此任务委托给 OSUser 框架。在 JIRA 中,Seraph 还做了一件额外的事情,即拦截以 /admin/ 开头的 URL,并且只有具备“全局管理员”权限的用户才能访问。

回到认证和其他用户管理功能,OSUser 在 JIRA 4.3 版本之前执行这些任务。它执行以下活动:

  • 用户管理——创建/更新/删除用户并将详细信息存储在 JIRA 数据库中。存储用户偏好设置。

  • 群组管理——创建/更新/删除群组并将详细信息存储在 JIRA 数据库中。管理群组成员。

  • 认证——密码匹配。

从 JIRA 4.3 开始,JIRA 中的用户管理通过 Crowd 完成。Crowd 是 Atlassian 提供的单点登录和身份管理系统,现在已集成在 JIRA 4.3 及以上版本中。插件开发者现在可以使用 CrowdService 来管理用户和组,更多信息请参考 docs.atlassian.com/atlassian-crowd/current/com/atlassian/crowd/embedded/api/CrowdService.html

属性管理

JIRA 允许您将键/值对作为属性添加到任何可用的“实体”上,如用户、组、项目、问题等。它使用 OpenSymphony 的 PropertySet 来实现这一点。JIRA 内部使用 PropertySet 的三个主要场景如下:

  • 存储 OSUser 框架中的用户首选项,如电子邮件、全名等

  • 存储应用程序属性

  • 存储用户仪表板上 Portlets/Gadgets 的首选项

我们还可以在插件中使用 PropertySet 来存储自定义数据作为键/值对。

在 JIRA 的早期版本中,PropertySet 是唯一用于存储插件信息和其他与插件相关数据的技术。但现在 JIRA 支持一种名为 ActiveObjects 的新技术,可以用来存储插件数据。相关内容将在后续章节中详细解释。

展示层

JIRA 的展示层是使用 JSPVelocity 模板构建的。进入 JIRA 的 Web 请求由 OpenSymphony 的 Webwork1 框架处理。这些请求由 Webwork 动作处理,内部使用 JIRA 服务层。服务类暴露出核心的实用工具类和管理类,执行背后的任务!

数据库

JIRA 通过 Ofbiz Entity Engine 模块与其数据库进行通信。其数据库模式定义在 entitmodel.xml 文件中,该文件位于 WEB-INF/classes/entitydefs 目录下。数据库连接配置位于 WEB-INF/classes 目录下的 entityengine.xml 文件中。

JIRA 支持多种数据库产品,更多详细信息请参见 confluence.atlassian.com/display/JIRA/Connecting+JIRA+to+a+Database

工作流

工作流是 JIRA 中最重要的功能之一。它提供了一个高度可配置的工作流引擎,背后使用了 OpenSymphony 的 OSWorkflow。它让我们通过添加新步骤和转换来定制工作流,并且对于每个转换,我们可以添加条件、验证器或后置功能。我们甚至可以编写插件,除了 JIRA 默认提供的功能外,添加更多功能。接下来的章节将详细讲解这些内容。

搜索

JIRA 使用 Apache Lucene 来执行索引操作。每当 JIRA 中的某个问题发生变化时,它会执行部分重新索引,以更新相关索引。JIRA 还允许我们在管理界面手动执行完整的重新索引。

在 JIRA 中的搜索是通过这些索引进行的,索引存储在本地硬盘上。我们甚至可以将搜索查询存储为过滤器,其结果会随着索引的变化而更新。

定时任务

JIRA 使用 Quartz API 在 JIRA 内部安排任务。这些任务,包括对过滤器的订阅和我们添加的自定义任务,都存储在 JIRA 数据库中,由 Quartz 作业调度服务执行。

JIRA 内建的定时任务详情可以在 scheduler-config.xml 中找到。

使用 SAL 服务实现可以在 JIRA 中安排新的事件。如同 Atlassian 所说:

"共享访问层(Shared Access Layer,简称 SAL)为常见插件任务提供一致的、凝聚的 API,无论你的插件部署到哪个 Atlassian 应用程序中。"

关于使用 SAL 在 JIRA 中安排事件的更多信息,可以在 developer.atlassian.com/display/DOCS/Plugin+Tutorial+-+Scheduling+Events+via+SAL 找到。

插件

最后但同样重要的是,插件适配到 JIRA 架构中,以提供额外的功能或修改一些现有的功能。这些插件大多数使用与 WebWork 操作相同的 JIRA 核心工具类和管理类,但在某些情况下也会添加或贡献到列表中。

在 JIRA 中有多个插件点,我们将在本章中详细介绍。

我希望这能为你提供一个简要的 JIRA 架构和其中使用的主要组件的介绍。我们将在接下来的章节中详细了解这些内容,并学习如何通过编写插件来定制它们。祝你好运!

插件模块类型

让我们简要了解一下 JIRA 4.x 中支持的不同类型的插件模块。所有这些模块都是各种扩展点,通过这些扩展点,我们不仅可以为 JIRA 添加新功能,还可以扩展一些现有的功能。

让我们按功能对它们进行分组,而不是一起查看它们!

报告

模块类型 描述
Portlet 向用户的仪表盘添加新的 portlet。该功能在 JIRA 4.x 中已弃用,但仍受支持。推荐使用 Gadget。
Gadget 向用户的仪表盘添加新的 Gadget。这些 Gadget 也可以从其他应用程序访问。
Report 向 JIRA 添加新的报告。

工作流

模块类型 描述
workflow-condition 向 JIRA 工作流添加新的工作流条件。它可以用于根据预定义条件限制用户执行工作流动作。
workflow-validator 向 JIRA 工作流添加新的工作流验证。验证可以用于在不满足条件时防止某些工作流动作。
workflow-function 向 JIRA 工作流添加新的工作流后置功能。这些功能可用于在工作流动作执行后执行自定义操作

自定义字段

模块类型 描述
customfield-type 向 JIRA 添加新的自定义字段类型。我们可以自定义字段的外观和行为,以及自定义逻辑。另见customfield-searcher

搜索

Module Type 描述
customfield-searcher 为 JIRA 添加新的字段搜索器。搜索器需要与相关的自定义字段映射。
jqlfunction 添加新的 JQL 函数,供 JIRA 的高级搜索使用。
search-request-view 在问题导航器中添加新的视图。它们可以用来以不同的方式显示搜索结果。

链接和选项卡

Module Type 描述
issue-operation 查看问题屏幕中添加新的问题操作。此模块在 JIRA 4.1.x 及以后版本中不可用。从 4.1.x 版本开始,使用 Web Items(参见web-item模块),而不是 issue-operation 模块。
web-section 在应用菜单中添加新的部分。每个部分下可以包含一个或多个链接。
web-item 添加新的链接,显示在定义的部分中。这里的部分可以是我们添加的新部分或现有的 JIRA 网页部分。
project-tabpanel 为浏览项目屏幕添加新选项卡。我们可以定义在选项卡中显示的内容。
component-tabpanel 为浏览组件屏幕添加新选项卡。如上所述,我们可以定义在选项卡中显示的内容。
version-tabpanel 为浏览版本屏幕添加新选项卡。同上。
issue-tabpanel 查看问题屏幕添加新选项卡。与其他选项卡类似,我们也可以定义在选项卡中显示的内容。
web-panel JIRA 4.4 中新增的功能。定义可以插入 HTML 页面的面板或部分。

远程调用

Module type 描述
rest 为 JIRA 创建新的 REST API,暴露更多服务和数据实体。
rpc-soap 为 JIRA 发布新的 SOAP 端点。它作为新的 SOAP 服务部署,并暴露一个新的 WSDL,包含我们在插件中发布的操作。
rpc-xmlrpc 同上。暴露 XML-RPC 端点,而不是 SOAP,供 JIRA 使用。

动作和组件

Module type 描述
webwork 向 JIRA 添加新的 webwork 操作和视图,这些操作可以添加新功能或覆盖现有功能。
component 向 JIRA 的组件系统添加组件。这些组件可以在其他插件中使用,并可注入其中。
component-import 导入其他插件共享的组件。

其他插件模块

Module type 描述
resource 将可下载的资源添加到插件中。资源是非 JAVA 文件,如 JavaScript、CSS、图像文件等。
web-resource 与上述类似,将可下载的资源添加到插件中。但这些资源会被添加到页面顶部,并设置缓存相关的头信息为永不过期。我们还可以指定这些资源仅在特定上下文中使用。多个资源模块会出现在 web-resource 模块下。
servlet 将一个 JAVA servlet 部署到 JIRA 上。
servlet-context-listener 部署一个 JAVA Servlet 上下文监听器。
servlet-context-param 设置插件的 servlet、过滤器和监听器共享的 Servlet 上下文中的参数。
servlet-filter 将 JAVA servlet 过滤器部署到 JIRA 上。可以指定该过滤器在应用程序过滤器链中的顺序和位置。
user-format 为用户详细信息添加自定义行为。用于增强用户资料。
keyboard-shortcut 仅适用于 4.1.x 及以上版本。定义 JIRA 的新键盘快捷键。您还可以覆盖 JIRA 4.2.x 中的现有快捷键!
module-type 动态地向插件框架添加新的插件模块类型。其他插件可以使用新模块。

atlassian-plugin.xml中包含什么内容?

让我们更深入地了解名为atlassian-plugin.xml的插件描述符。 |

以下是创建骨架插件时插件描述符的样子: |

<atlassian-plugin key="${project.groupId}.${project.artifactId}" name="${project.artifactId}" plugins-version="2">
  <plugin-info>
    <description>${project.description}</description>
    <version>${project.version}</version>
    <vendor name="${project.organization.name}"url="${project.organization.url}" />
  </plugin-info>
</atlassian-plugin>

我们需要根据要开发的插件类型添加更多的细节。插件描述符可以分为三部分: |

  1. Atlassian-plugin元素:这是描述符的根元素。以下属性会填充atlassian-plugin元素: |

    • key:这可能是最重要的部分。它应该是 JIRA 实例中唯一的键,用来引用插件中的不同模块,就像我们在 Java 应用程序中使用包一样。如果插件键显示为${project.groupId}.${project.artifactId},它会从pom.xml文件中获取值。当插件构建时,插件键将是YOUR_GROUP_ID.YOUR_ARTIFACT_ID。 |

    • name:为您的插件起一个合适的名称。该名称将出现在管理菜单下的插件列表中。 |

    • plugins-version:这与版本属性不同。plugins-version定义插件的版本是 1 还是 2。plugins-version="2"定义该插件为版本 2 插件。要将其设置为版本 1 插件,请删除整个属性。 |

    • state:这是一个可选元素,用于将插件定义为禁用状态,默认情况下为禁用。添加state="disabled"atlassian-plugin元素下。 |

  2. Plugin-info元素:该部分包含插件的信息。它不仅提供显示给管理员的信息,还可选择性地为 OSGI 网络提供捆绑指令: |

    • description:关于您的插件的简短描述。 |

    • version:插件的实际版本,将与名称和描述一起显示在插件菜单中。 |

    • application-version:在这里,你可以定义插件所支持的 JIRA 应用程序的最小和最大版本。<application-version min="4.0.2" max="4.1"/>将支持从 4.0.2 到 4.1 的版本。但请记住,这仅供参考,插件可能在 JIRA 4.2 中仍然正常工作!

    • vendor:在这里,你可以提供有关插件供应商的详细信息。它支持两个属性:nameurl,可以分别填入组织的名称和 URL。类似于插件键,你可以从pom.xml文件中填充这些属性,就像你在骨架描述符中看到的一样。

    • param:此元素可用于定义插件的名称/值属性。你可以传递任意数量的属性。例如,<paramname="configure.url">/secure/JTricksConfigAction.jspa</param>定义了我们演示插件的配置 URL。

    • bundle-instructions:在这里,我们定义了 OSGI 捆绑包指令,这些指令将在生成 OSGI 捆绑包时由 Maven Bundle 插件使用。更多内容可以在 aQutebndtool 下阅读:www.aqute.biz/Code/Bnd。以下是快照中的两个元素:

      • Export-Package:此元素定义了此插件中可以暴露给其他插件的包。所有其他包将保持私有。

      • Import-Package:此元素定义了此插件之外但在其他插件中导出的包。

  3. Plugin Modules:这是实际插件模块所在的部分,前面我们稍微提到过,稍后在本书中会详细讨论。

希望你现在已经准备好插件描述符,包含了所有必要的属性!

使用 Plugins1 和 Plugins2 版本

让我们快速了解如何处理 Plugins1 和 Plugins2 版本。

在我们深入细节之前,理解两个版本的重要性至关重要。在 4.x 版本之后,JIRA 只支持 Plugins1 版本。那么,为什么我们还需要 Plugins2 版本呢?

版本 2 插件的主要目的是将插件作为捆绑包与其他插件和 JIRA 核心类隔离开来。它利用 OSGI 平台(www.osgi.org)来实现这一点。虽然它使插件保持隔离,但它也为你定义插件间的依赖关系提供了便利,且方便插件开发者使用。它甚至允许你在插件中导入或导出选定的包,从而提供了更大的灵活性。

版本 2 插件作为 OSGI 捆绑包部署的事实也意味着插件具有动态特性。在框架运行期间,插件可以随时安装、启动、更新、停止和卸载。

开发者可以根据插件的性质选择使用 Plugins1 版本或 Plugins2 版本。

让我们来看看插件开发过程中两个版本在不同阶段的关键区别。

Development

Plugins1 Plugins2
版本 atlassian-plugin.xml 中没有 plugins-version 元素。 atlassian-plugin.xml 中包含 plugins-version 元素,格式如下:<atlassian-plugin key="${project.groupId}.${project.artifactId}" name="${project.artifactId}" plugins-version="2">

| 外部依赖 | 如果 JAR 文件被添加到 WEB-INF/lib,则在 pom.xml 文件中以 provided 作用域包含依赖库;如果 JAR 文件应嵌入到插件中,则以 compile 作用域包含依赖库。 | 依赖库必须包含在插件中,因为插件不能使用位于 WEB-INF/lib 下的资源。可以通过两种方式完成此操作。

  • pom.xml 文件中提供作用域为 compile。在这种情况下,JAR 文件将被插件 SDK 拾取并添加到插件的 META-INF/lib 文件夹中。

  • 手动将依赖的 JAR 文件添加到插件内部的 META-INF/lib 目录中。

你也可以让插件依赖其他捆绑包。请参见本表中的 管理复杂依赖关系

依赖注入 由 JIRA 中的 Pico 容器完成。所有注册的组件都可以直接注入。 由插件框架完成。并非所有 JIRA 核心组件都可以在构造函数中进行注入。使用 component-import 模块来访问插件框架中不可直接访问的一些依赖项。也可以用它来导入其他插件中声明的公共组件。
声明新组件 使用组件模块注册新组件。完成后,它对所有插件都可用。 使用组件模块注册组件。要使其对其他插件可用,将 public 属性设置为 'true'。默认情况下是 'false',使其仅对声明它的插件可用。
管理复杂依赖关系 版本 1 的插件中的所有类对所有其他插件和 JIRA 核心类可用。 版本 2 插件允许我们通过在插件描述文件中使用 bundle-instructions,或者在构建捆绑包时使用 Import-Package/Export-Packge 选项,选择性地导入/导出选定的包。因此,捆绑包依赖系统允许你定义插件之间的复杂依赖关系,消除类路径冲突并升级插件。

安装

插件 1 插件 2
插件必须位于应用程序类路径上。因此,将其部署在 WEB-INF/lib 文件夹下。 插件不能位于应用程序类路径上。它通过插件框架加载。因此,插件被部署在 ${jira-home}/plugins/installed-plugins/ 下。Jira-homeWEB-INF/classes 下的 jira-application.properties 文件中声明。

好的,我们现在知道了两个插件版本的工作原理。也许是时候看看 JIRA 自带的插件了!

JIRA 系统插件

在本节中,我们将简要概述 JIRA 系统插件。

JIRA 的许多功能是以插件的形式编写的。它不仅展示了我们通过插件可以实现的功能,还帮助我们作为开发人员理解各个组件是如何结合在一起的。

如果是atlassian-plugin.xml文件描述插件功能,JIRA 会在WEB-INF/classes下的*.xml文件中维护相关信息。你还可以在WEB-INF/classes下的爆炸文件夹中找到相关的类。

让我们快速了解在WEB-INF/classes中可以找到的各种系统插件 XML 及其支持的功能:

系统插件 XML 功能

| system-contentlinkresolvers-plugin.xml | 系统内容链接解析器—将解析后的内容链接转换为链接对象。

  • 附件链接解析器

  • 锚点链接解析器

  • JIRA 问题链接解析器

  • 用户档案链接解析器

|

| system-customfieldtypes-plugin.xml | JIRA 系统自定义字段—JIRA 中所有开箱即用的自定义字段及其搜索器关联。示例:

  • 文本字段

  • 文本区域

  • ...............

  • 用户选择器

  • 选择

|

system-footer-plugin.xml 此插件渲染 JIRA 中的页脚内容。

| system-issueoperations-plugin.xml | 系统问题操作—使用 web-items 和 web-sections 对问题操作进行分组并渲染。示例:

  • 编辑问题

  • 分配问题

  • ..................

  • 日志工作

|

| system-issuetabpanels-plugin.xml | 系统问题标签面板—渲染查看问题页面上的各种标签:

  • 所有标签面板

  • 评论标签面板

  • 工作日志标签面板

  • 更改历史标签面板

  • CVS 标签面板

|

| system-issueviews-plugin.xml | 渲染单个问题视图和各种搜索请求视图

  • 单个问题视图:XML、Word、可打印

  • 搜索视图:XML、RSS、RSS(评论)、可打印、Word、完整内容、Excel(所有字段)、Excel(当前字段)、图表

|

system-jql-function-plugin.xml 内置JQL 函数
system-keyboard-shortcuts-plugin.xml 内置键盘快捷键
system-macros-plugin.xml JIRA 的基础系统宏
system-portlets-plugin.xml 内置端口小部件
system-project-plugin.xml 系统项目面板—渲染浏览项目、浏览版本和浏览组件面板。
system-projectroleactors-plugin.xml 系统项目角色演员—内置的项目角色演员(用户角色演员和组角色演员)及其相关的 webwork 动作。
system-renderercomponentfactories-plugin.xml 渲染器组件工厂插件—使用插件系统实例化渲染器组件,如宏渲染器、链接渲染器、URL 渲染器等。

| system-renderers-plugin.xml | 内置系统渲染器

  • Wiki 样式渲染器

  • 默认文本渲染器

|

system-reports-plugin.xml 内置系统报告
system-top-navigation-plugin.xml 渲染 JIRA 中的顶部导航栏内容。包含一组 web-items 和 web-sections。
system-user-format-plugin.xml 在 JIRA 的不同地方以不同的方式渲染用户。
system-user-profile-panels.xml 渲染用户档案页面上的面板。
system-webresources-plugin.xml 系统 Web 资源—包括静态资源,如 JavaScript 文件、样式表等。
system-webwork1-plugin.xml 系统 webwork 插件—可以用于添加自定义的 webwork 动作,这也可以通过插件来完成。
system-workflow-plugin.xml 系统工作流条件功能验证器

除了将这些文件作为 JIRA 插件开发的起点外,我们有时还会修改这些文件,以覆盖 JIRA 的默认工作方式。

在 JIRA 升级过程中,必须注意进行变更升级。

这就是对 JIRA 架构的相当长的介绍!让我们快速进入本章的内容吧。是时候编码了!!

将插件从 v1 转换为 v2

如果你从 JIRA 3.13.x 或更早版本迁移到 JIRA 4.x,一个重要的区别是引入了 v2 插件。在设计 JIRA 4.x 的升级时,有时将插件从 v1 迁移到 v2 是完全合理的,尽管这不是一个强制步骤。在这个教程中,我们将看到如何将 v1 插件转换为 v2 插件。

准备工作

在转换插件之前,我们需要问几个问题:

  • 插件使用的所有包是否都可以用于 OSGi 插件? 这是非常重要的,因为 JIRA 并没有将所有包暴露给 OSGi 插件。

    可用的包列表可以在com.atlassian.jira.plugin.DefaultPackageScannerConfiguration类中找到,这些包已经导出并可用于 plugins2 版本。

  • 插件使用的所有组件是否都可以用于 OSGi 插件? 类似于前一个问题,我们需要确保这些组件也暴露给 OSGi 插件。

    不幸的是,Atlassian 并没有为 JIRA 提供一个明确的组件列表。为了检查组件是否可用,可以使用依赖注入。如果组件不可用,插件将在启动时失败。

如何操作...

如果插件中使用的包和组件可供 OSGi 插件使用,那么将 v1 插件转换为 v2 的实际过程比你想象的要简单。以下是转换的步骤。

  1. atlassian-plugin.xml中添加plugins-version="2"属性。这可能是转换过程中的唯一强制步骤。你会惊讶地发现,许多插件会直接工作!一旦添加,插件描述符看起来如下:

    <atlassian-plugin key="${project.groupId}.${project.artifactId}" name="Demo Plugin" plugins-version="2">
    .....................
    </atlassian-plugin>
    
  2. 如果需要,修改源代码。这包括迁移到新的 API,如果你正在迁移到一个带有 API 更改的新 JIRA 版本,处理那些在 v1 插件中使用的、没有导出到 OSGi 的包/组件的更改等等。

  3. 通过在包清单中定义,来自定义包的导入和导出。你可以通过使用我们在本章前面讲解atlassian-plugin.xml时看到的包指令来完成,或者简单地将适当的条目添加到你的 JAR 文件中的清单文件里。

    这是一个可选步骤,仅在你需要从另一个插件/包导入包,或者需要导出一些包以供其他插件使用时,才需要执行此操作。

  4. 使用component模块将你的自定义插件组件暴露给其他插件。你必须在atlassian-plugin.xml文件中注册的组件中设置public属性为true。也就是说,public="true"

    如果你想使用其他插件中公开声明的组件,你必须明确导入这些组件。可以使用component-import模块来实现这一点。

    <component-import key="democomponent" interface="com.jtricks.DemoComponent" />
    
  5. 你还可以通过在META-INF/spring/目录下添加Spring 动态模块SpringDM)配置文件(格式为*.xml),可选择性地添加高级 Spring 配置。这些文件将由 Spring DM 加载器加载。具体细节超出了本书的范围。

它是如何工作的...

使用 Atlassian 描述符创建的 v2 插件 JAR 文件,包含所需的模块,经过以下过程:

  1. 插件会在 JIRA 启动时加载,JIRA 会识别新的 JAR 文件。

  2. DirectoryLoader检查新插件是版本 2 还是版本 1。

  3. 如果是版本 2,它会检查MANIFEST.MF文件中的 OSGI 清单条目。如果找到了,它会将插件安装为 OSGI 包并启动。

  4. 如果没有 OSGI 清单条目,JIRA 会使用 BND 工具(www.aqute.biz/Code/Bnd)来生成清单条目,并将它们插入到MANIFEST.MF文件中。

  5. 然后它会检查是否存在显式的atlassian-plugin-spring.xml文件。如果该文件存在,插件将作为 OSGI 包部署,如步骤 2 所示。

  6. 如果atlassian-plugin-spring.xml文件不存在,它会扫描atlassian-plugin.xml文件,并将注册的组件和其他内容转换为 OSGI 引用或 OSGI 服务,然后创建一个atlassian-plugin-spring.xml文件。

  7. 一旦atlassian-plugin-spring.xml文件创建完成,插件就作为 OSGI 包部署并安装到 PluginManager 中。

因此,JIRA 使我们能够灵活地定义自定义的 OSGI 清单条目和引用,或者让 JIRA 通过在插件描述符中适当地定义它们来完成繁琐的工作。

另见

  • 在第一章中部署插件插件开发流程

  • 在第一章中创建一个骨架插件

将资源添加到插件中

在我们的插件中,通常需要添加静态资源,如 JavaScript 文件、CSS 文件等。为了让 JIRA 能够提供这些附加的静态文件,它们应该定义为可下载的资源。

准备工作

资源可以有不同类型。通常定义为插件运行所需的非 Java 文件。

在 JIRA 插件开发过程中,你将遇到的资源示例包括但不限于以下内容:

  • 渲染视图所需的 Velocity (*.vm) 文件

  • JavaScript 文件

  • CSS 文件

  • 本地化的属性文件

如何操作...

要包含资源,请将资源模块添加到atlassian-plugin.xml文件中。资源模块可以作为整个插件的一部分添加,或者可以包含在另一个模块中,仅限该模块使用。

以下是资源模块可用的属性和元素及其用途:

名称 描述
name 资源的名称。插件或模块使用该名称定位资源。你甚至可以通过添加末尾的/将目录定义为资源。
namePattern 加载目录资源时使用的模式。

| type | 资源的类型。例如:

  • download用于像 CSS、JavaScript、图像等资源

  • velocity用于 velocity 文件

|

location 资源在插件 JAR 中的位置。需要提供文件的完整路径,但不包含前导斜杠。使用时,namePattern或者指向目录资源时,末尾需要加斜杠/
property (key/value) 用于将属性作为键/值对添加到资源中。作为子标签添加到 resources 中。例如:<property key="content-type" value="text/css"/>
param (name/value) 用于添加名称/值对。作为子标签添加到 resources 中。例如:<param name="content-type" value="image/gif"/>

你只需将资源标签添加到atlassian-plugin.xml文件中,无论是在插件级别还是模块级别。然后,资源就可以使用了。

图片的资源定义如下所示:

<resource type="download" name="myimage.gif" location="includes/images/ myimage.gif">
  <param name="content-type" value="image/gif"/>
</resource>

CSS 文件可能如下所示:

<resource type="download" name="demostyle.css" location="com/jtricks/ demostyle.css"/>

一旦资源在插件描述文件中定义,你就可以在插件中的任何地方使用它。以下是如何引用该资源。

假设你有一个目录,如下所示:

<resource type="download" name="images/"location="includes/images/"/>

文件demoimage.gif可以在你的 velocity 模板中如下引用:

$requestContext.baseUrl/download/resources/${your_plugin_key}:${module_key}/images/ demoimage.gif

插件模块中使用的示例代码如下所示:

<img id="demo-image" src="img/ demoimage.gif"/>

其中com.jtricks.demo是插件密钥,demomodule是模块密钥。

将 Web 资源添加到插件中

Web 资源插件模块,像我们刚刚看到的资源模块一样,允许定义可下载的资源。不同之处在于,Web 资源被添加到页面顶部的头部,并且缓存相关的头部被设置为永不过期。

使用 Web 资源模块的另一个优点是,可以指定在应用程序中的特定上下文中包含哪些资源。

如何操作...

Web 资源插件模块的根元素是web-resource。它支持以下属性:

名称 描述
Key 唯一的必需属性,应该在插件内唯一。
Disabled 指示插件模块是否默认禁用。
i18n-name-key 插件模块的人类可读名称的本地化键。
Name web 资源的可读名称。

以下是支持的关键元素。

Name 描述
description 模块的描述。
resource 要添加为 web 资源的所有资源。见将资源添加到插件中
dependency 用于定义对其他 web-resource 模块的依赖。依赖项应定义为pluginKey:web-resourceModuleKey。示例:<dependency>com.jtricks.demoplugin:demoResource</dependency>
context 定义 web 资源可用的上下文。

我们可以通过适当填充属性和元素来定义 web-resource 模块。

一个示例看起来如下:

<atlassian-plugin  name="Demo Plugin" key="com.jtricks.demoplugin" plugins-version="2">
  <plugin-info>
    <description>Demo Plugin for web-resources</description>
    <vendor name="J Tricks"url="http://www.j-tricks.com"/>
    <version>1.0</version>
  </plugin-info>
 <web-resource key="demoresource" name="Demo">
    <resource type="download" name="demoscript.js" location="includes/
      js/demoscript.js" />
    <resource type="download" name=" demoscript1.js" 
      location="includes/js/demoscript1.js" />
  </web-resource>
</atlassian-plugin>

它是如何工作的...

当定义一个 web 资源时,它就像你可下载的插件资源一样可用。正如前面提到的,资源会被添加到页面顶部的头部部分。

在你的操作类或 servlet 中,你可以通过WebResourceManager来访问这些资源。将管理类注入到构造函数中,然后你可以用它来定义资源,如下所示:

webResourceManager.requireResource("com.jtricks.demoplugin: demoresource");

参数应为pluginKey:web-resourceModuleKey

默认情况下,web-resource 模块下的所有资源都以批处理模式提供,即通过单个请求。这可以减少来自网页浏览器的 HTTP 请求次数。

还有更多...

在我们结束这段教程之前,最好先识别 web 资源的可用上下文,并看看如何在加载资源时关闭批处理模式。

web 资源上下文

以下是可用的 web 资源上下文:

  • atl.general:除管理屏幕外的所有地方

  • atl.admin:管理屏幕

  • atl.userprofile:用户个人资料屏幕

你可以像这样添加多个上下文:

<web-resource key="demoresource" name="Demo">
    <resource type="download" name="demoscript.js" location="includes
/js/ demoscript.js" />
    <context>atl.general</context>
    <context>atl.admin</context>
</web-resource>

关闭批处理模式

如前所述,资源会以批处理方式加载,以减少浏览器的 HTTP 请求次数。但如果你因某些原因想关闭批处理模式,可以通过两种方式实现。

  1. 你可以通过将属性plugin.webresource.batching.off=true添加到jira-application.properties中,来全局关闭批处理模式。

  2. 它可以通过为单个资源添加param元素来关闭,如下所示:

    <resource type="download"  name="demoscript.js"  location="includes/js/ demoscript.js">
        <param name="batch" value="false"/>
    </resource>
    

另见

  • 将资源添加到插件中

从源代码构建 JIRA

JIRA 的一个最棒的特点是,如果你拥有有效的许可证,你可以查看源代码。想看就看,想修改就修改,想破坏...呃,修改它,因为你有许可证这么做!

准备就绪

以下是从源代码构建 JIRA 之前的一些前提条件。

  • 获取有效的 JIRA 许可证以访问源代码。

  • 对于 JIRA 4.2 及以下版本,需要 JDK 1.5 或更高版本。JIRA 4.3 及以上版本需要 JDK 1.6 或更高版本。

  • 如果你正在构建 JIRA 4.3 之前的版本,你需要同时使用 Maven1 和 Maven2。可以从 maven.apache.org 下载 Maven 1.0.x 和 2.1.x。JIRA 4.3 及以上版本只需要 Maven 2.1.0。

注意

对于 JIRA 4.3 之前的版本,你需要同时使用 Maven1 和 Maven2,因为 Maven1 用于构建 JIRA 源码,而 Maven2 用于构建 JIRA 的插件。JIRA 有捆绑的插件,这些插件需要与 JIRA 一起构建,因此 Maven2 也是必须的。

插件开发过程需要 Maven 2.1.0 及以上版本。

如何操作...

让我们看看从源码构建 JIRA WAR 的步骤,适用于 JIRA 4.3 之前的版本:

  1. 配置 Maven 1.0.x

    • 将之前下载的 Maven 1.0.x 版本解压到一个目录,我们将其称为 MAVEN_INSTALL_DIR

    • http://confluence.atlassian.com/download/attachments/185729661/ant-optional-1.5.3-1.jar?version=1&modificationDate=1276644963420 下载一个 Atlassian 修补过的 Ant jar 版本,并将其复制到 MAVEN_INSTALL_DIR/maven-1.0/lib

    • 设置 MAVEN_HOME 环境变量,它将是 MAVEN_INSTALL_DIR/maven-1.0

    • 将 Maven 的 bin 目录添加到路径变量中。

  2. 配置 Maven 2.1.x。如果你已经使用 Atlassian Plugin SDK 设置了开发环境,可以跳过此测试,因为它自带了一个捆绑的 Maven 2.x。

  3. www.atlassian.com/software/jira/JIRASourceDownloads.jspa 下载 JIRA 源码的 ZIP 文件。

  4. 将 JIRA 源码解压到一个目录,我们称之为 JIRA_DIR

  5. 进入 jira 子目录,也就是 JIRA_DIR/jira

  6. 运行以下命令来创建一个开放的 WAR:

    maven war:webapp
    
    

    如果你想创建一个封闭的 WAR,执行以下操作:

    maven war:war
    
    
  7. 请参见 maven.apache.org/maven-1.x/plugins/war/goals.html 了解更多 Maven WAR 目标。

  8. 确认 WAR 是否正确创建。

以下是在 JIRA 4.3 及更高版本中创建 WAR 的步骤。

  1. 配置 Maven 2.1.0。

  2. 下载并安装所需的第三方库,因为这些库在公共 Maven 仓库中不可用:

    • 下载以下提到的正确版本的 jar 文件:

      activation javax.activation:activation 1.0.2
      jms javax.jms:jms 1.1
      jmxri com.sun.jmx:jmxri 1.2.1
      jmxtools com.sun.jdmk:jmxtools 1.2.1
      jndi jndi:jndi 1.2.1
      jta Jta:jta 1.0.1B
      mail javax.mail:mail 1.3.2
    • 使用 Maven 安装命令将它们安装到本地 Maven 仓库:

      mvninstall:install-file -DgroupId=javax.activation -DartifactId=activation -Dversion=1.0.2 -Dpackaging=jar -Dfile=activation-1.0.2.jar
      mvninstall:install-file -DgroupId=javax.jms -DartifactId=jms -Dversion=1.1 -Dpackaging=jar -Dfile=jms-1.1.jar
      mvninstall:install-file -DgroupId=com.sun.jmx -DartifactId=jmxri -Dversion=1.2.1 -Dpackaging=jar -Dfile=jmxri.jar
      mvninstall:install-file -DgroupId=com.sun.jdmk -DartifactId=jmxtools -Dversion=1.2.1 -Dpackaging=jar -Dfile=jmxtools.jar
      mvninstall:install-file -DgroupId=jndi -DartifactId=jndi -Dversion=1.2.1 -Dpackaging=jar -Dfile=jndi.jar
      mvninstall:install-file -DgroupId=jta -DartifactId=jta -Dversion=1.0.1 -Dpackaging=jar -Dfile=jta-1_0_1B-classes.jar
      mvninstall:install-file -DgroupId=javax.mail -DartifactId=mail -Dversion=1.3.2 -Dpackaging=jar -Dfile=mail.jar
      
      
  3. 将 JIRA 源代码归档解压到本地目录,我们称之为JIRA_DIR

  4. 转到提取的子目录,目录名为atlassian-jira-X.Y-source,其中 X.Y 为版本号。

  5. 如果你在 Windows 上,运行build.bat;如果在 Linux 或 Mac 上,运行build.sh

  6. 确保 WAR 文件已正确创建,并位于JIRA_DEV/jira-project/jira-distribution/jira-webapp-dist/target子目录下。

它是如何工作的……

正如你所看到的,整个过程非常简单,实际的构建是由 Maven 这个魔术师完成的。

JIRA 随附project.xmlpom.xml文件(如果版本为 4.3 或更高),称为项目对象模型,用于 Maven 构建 WAR 文件。

你可以在project.xml / pom.xml中找到 JIRA 的依赖项。Maven 将首先构建这些依赖项,然后使用它们来构建 JIRA 的 WAR 文件。

这里唯一需要注意的关键点是正确配置 Maven。构建 JIRA WAR 时通常会遇到几个与 Maven 相关的问题。或许在继续之前,我们可以简要提一下这些问题。

  • 下载依赖项时出现错误,异常信息为java.net.ConnectException: 连接超时:连接

    如果遇到此问题,请确保 Maven 代理设置已正确配置。如果已经配置且仍然出现错误,尝试禁用你的防病毒软件!

  • 未能解析工件错误。构建 JIRA 4.0 时无法下载 javax jms jar。在这种情况下,手动下载 jar 文件并使用mvn install将其安装到本地仓库。

    mvninstall:install-file -Dfile=<path-to-file> -DgroupId=<group-id> -DartifactId=<artifact-id> -Dversion=<version> -Dpackaging=<packaging>
    

    在 4.3+版本中,参照配方中的步骤 2,其中给出了相关的mvn install命令。

一旦 WAR 文件创建完成,将其部署到支持的应用服务器中,享受 JIRA 的强大功能!

提示

下载本书的示例代码

你可以从www.PacktPub.com的账户中下载所有你购买的 Packt 书籍的示例代码文件。如果你在其他地方购买了这本书,可以访问www.PacktPub.com/support并注册以直接将文件通过电子邮件发送给你。

还有更多……

除了 JIRA 源代码外,我们还可以访问 Atlassian 的一些 JIRA 依赖项的源代码。如果你希望修改它们的行为,可能需要单独构建这些依赖项。

构建 JIRA 依赖项

类似于 JIRA,依赖项目也使用 Maven。但在某些情况下使用 Maven1,而在其他情况下使用 Maven2。

你可以通过检查其项目对象模型(POM),查看根目录中的文件来确定依赖项使用的是 Maven1 还是 Maven2。如果文件名为project.xml,则使用 Maven1;如果文件名为pom.xml,则使用 Maven2。很简单,对吧?

如果依赖项使用 Maven1,请使用以下命令生成 jar 文件。

maven jar

对于使用 Maven2 的依赖项,使用:

mvn package

另见

  • 在第一章中,设置开发环境

向 JIRA 添加新 webwork 操作

大多数时候,插件开发人员会发现自己在 JIRA 中编写新的操作,以引入新功能。通常,这些操作是从 UI 中不同位置配置的新 web-item 链接调用的,也可以通过自定义 JSP 或 JIRA 框架的其他部分来调用。

可以通过 webwork 插件模块向 JIRA 添加新操作。

准备开始

在我们开始之前,查看一下 webwork 插件模块是有意义的。以下是支持的关键属性:

名称 描述
Key 插件中的唯一键。它将用作插件的标识符。
Class 这将是java.lang.Object,因为实际的逻辑将存在于操作类中。
i18n-name-key 插件模块的人类可读名称的本地化键。
名称 webwork 操作的可读名称。

以下是支持的关键元素:

名称 描述
description webwork 模块的描述。
actions 在这里我们指定 webwork1 操作。一个 webwork 模块必须至少包含一个操作元素。它可以包含多个操作。

对于每个 webwork1 操作,我们应填写以下属性:

名称 描述
name 操作类的完全限定名称。该类必须继承com.atlassian.jira.action.JiraActionSupport
alias 操作类的别名。JIRA 将使用此名称来调用操作。

以下元素支持 webwork1 操作:

名称 描述
view 根据操作的输出,将用户委派到适当的视图。此元素有一个属性:name,它映射到操作类的返回值。

现在你已经看到支持的属性和元素,我们可以在继续创建之前,先看看一个示例 webwork 模块!

<webwork1 key="demoaction" name="JTricks Demo Action" class="java.lang.Object">
  <actions>
    <action name="com.jtricks.DemoAction" alias="DemoAction">
      <view name="input">/templates/input.vm</view>
      <view name="success ">/templates/joy.vm</view>
      <view name="error">/templates/tears.vm</view>
    </action>
  </actions>
</webwork1>

如何操作...

现在,让我们创建一个示例 webwork 操作。作为示例,我们可以创建一个操作,接受用户输入,在控制台中打印,并在修改输入后显示在输出页面上。

以下是执行的步骤:

  1. 将新的 webwork 操作模块添加到你的atlassian-plugin.xml中。假设我们添加了上述相同的代码片段。

  2. com.jtricks包下创建操作类DemoAction。该类必须继承com.atlassian.jira.action.JiraActionSupport

  3. 确定你需要从用户接收的参数。为它们创建私有变量,名称应与相关 HTML 标签完全相同。

    在我们的示例中,我们需要获取用户输入。假设它是用户的名字。输入视图中的 HTML 代码(在我们的例子中是/templates/input.vm)如下:

    Name:  <input type="text" name="userName">
    

    因此,我们需要在我们的动作类中创建一个名为userName的字符串变量。

  4. 为用于从输入视图获取值的变量创建 setter 方法。

    在我们的示例中,我们从输入视图中获取userName并在动作类中处理它。因此,我们需要为此创建一个 setter 方法,方法如下:

    public void setUserName(String userName) {
      this.userName = userName;
    }
    
  5. 确定需要在输出页面中打印的参数。在我们的示例中,我们将打印modifiedName到输出页面。

  6. 为要打印的参数创建 getter 方法。Velocity 或 JSP 会调用 getter 方法从Action类中获取值。在我们的示例中,我们为modifiedName创建了一个 getter 方法,方法如下:

    public String getModifiedName() {
        return modifiedName;
    }
    
  7. 重写感兴趣的方法。这是实际逻辑的所在。插件开发人员可以完全根据插件的逻辑决定要覆盖哪些方法。

    需要关注的三个主要方法如下。但你可以完全省略这些方法并编写自己的命令和相关方法:

    • doValidation:这是执行输入验证的方法。插件开发人员可以覆盖此方法并添加我们自己的自定义验证。

    • doExecute:这是执行动作的地方。当输入表单提交时,如果没有验证错误,将调用doExecute方法。所有的业务逻辑都在这里处理,基于执行结果返回相应的'视图'名称。

      在我们的示例中,我们使用此方法修改输入的字符串:

      this.modifiedName = "Hi,"+userName;
      return "success";
      
    • doDefault:当使用'default'命令时会调用此方法。在我们的示例中,DemoAction!default.jspa将调用doDefault方法。

      在我们的示例中,我们使用此方法将用户重定向到输入页面:

      return "input";
      
  8. 为输入视图创建 Velocity 模板。在我们的示例中,'input'视图使用模板:/templates/input.vm。将输入文本的 HTML 代码添加到一个表单中,该表单的操作调用DemoAction

    <h2>My Input Form</h2><br><br>
    <form method="POST" action="/secure/DemoAction.jspa">
          Name:  <input type="text" name="userName"><br>
                 <input type="submit">
    </form>
    
  9. /templates/joy.vm中创建成功视图,打印modifiedName输出结果:$modifiedName

  10. /templates/error.vm中创建错误视图:哦不,出错了!

  11. 打包插件并部署它。

  12. 在浏览器中输入${jira_base_url}/secure/DemoAction!default.jspa。输入一个名称并提交表单,以查看它的实际效果!

    注意

    这里给出的示例仅仅是为了帮助理解 webwork 动作是如何工作的。

它是如何工作的...

值得利用此部分来查看在我们的示例中流程是如何工作的。让我们按步骤看看它是如何发生的。

  1. ${jira_base_url}/secure/DemoAction!default.jspa被调用时,插件框架会查找在atlassian-plugin.xml文件中注册的DemoAction动作,并识别与其相关的命令和视图。

  2. 在这里,default命令被调用,因此doDefault方法在动作类中执行。

  3. doDefault方法返回视图名称为input

  4. 输入视图解析为 input.vm,它向用户呈现表单。

  5. 在表单中,webwork 使用 setter 方法在 action 类中填充 userName 的值。

    在执行流程中,首先会调用 doValidation 方法。如果没有错误,正如我们示例中的情况,它将调用 doExecute 方法。

    如果在 doValidation 中发生任何错误,执行将停止并显示输入视图。如果有错误消息,可以在输入视图中适当打印它们。详情请见 webwork1 文档。

  6. 输入字符串 userName 随后在 action 类(即 doExecute 方法)中被修改,并赋值给 modifiedName,然后返回 success

  7. 成功视图解析为 joy.vm,其中会打印 modifiedName$modifiedName 将调用 getModifiedName() 方法来打印修改后的名称。

    如果返回错误,视图会解析为 error.vm,并显示适当的错误信息!

就像这样,我们可以在 JIRA 中编写复杂的动作,用于自定义 JIRA 的许多方面。

还有更多……

除了 doExecutedoDefault 方法外,还可以向 webwork 动作中添加自定义命令。这使得开发者可以通过用户友好的命令调用该动作,例如 ExampleAction!hello.jspa

向动作添加新命令

以下是如何在 webwork 动作模块中添加自定义命令的简短示例。

atlassian-plugin.xml 文件应被修改,以在 action 下包含新命令:

<action name="com.jtricks.DemoAction" alias="DemoAction">
    <view name="input">/templates/input.vm</view>
    <view name=" success ">/templates/joy.vm</view>
    <view name="error">/templates/tears.vm</view>
    <command name="hello" alias="DemoHello">
       <view name="success">/templates/hello.vm</view>
       <view name="error">/templates/tears.vm</view>
    </command>
</action>

在这种情况下,我们需要在 action 类中创建一个方法 doHello()

你可以通过调用 DemoAction!hello.jspa 来调用该方法,这时方法将被执行,并且返回的“success”消息将把用户带到 to /templates/joy.vm

你可以为命令创建单独的视图,并通过调用关联的别名 DemoHello.jspa 来调用命令。在这种情况下,返回的“success”将把用户带到 /templates/hello.vm

另见

  • 在 第一章 中部署你的插件

扩展 JIRA 中的 webwork 动作

这个问题有很多用户故事!如何覆盖一些 JIRA 内置的动作?如何在 JIRA 内置的动作中做一些额外的操作?(比如在页面返回给用户之前,立刻做一些疯狂的事情,或者对其中某些动作进行创新性的验证)

扩展现有的 JIRA 动作是解决所有这些问题的答案。让我们详细看看如何实现。

如何实现……

扩展 JIRA 动作是借助 webwork 插件模块完成的。大部分步骤与编写新的 webwork 动作非常相似。

让我们以 创建问题 动作为例。如果我们需要扩展创建动作,该怎么做呢?比如,执行一些额外的验证并在实际创建完成后做一些额外的操作?

以下是简要的步骤:

  1. 通过查找 JIRA 安装目录中的 WEB-INF/classes 下的 actions.xml 来识别需要被覆盖的操作。

    在我们的例子中,CreateIssueDetails 是负责创建 issue 的操作类:

    <action name="issue.CreateIssueDetails" alias="CreateIssueDetails">
       <view name="error">/secure/views/createissue-details.jsp</view>
       <view name="input">/secure/views/createissue-details.jsp</view>
    </action>
    

    这段代码定义了操作类和使用 JSP 文件的相关视图。

  2. 确定我们是否需要覆盖该操作,或者仅修改 JSP 文件。在我们的示例中,让我们进行一些额外的验证。

  3. atlassian-plugin.xml 中添加 webwork 插件模块:

    <webwork1 key="jtricks-create-issue-details" name="JTricks Create Issue Details">
      <actions>
        <action name="com.jtricks.MyCreateIssueDetails" alias="CreateIssueDetails">
          <view name="error">/secure/views/createissue-details.jsp</view>
          <view name="input">/secure/views/createissue-details.jsp</view>
        </action>
      </actions>
    </webwork1>
    
  4. 注意操作类名称的变化。如果需要,我们也可以修改 JSP 文件。但最重要的是,alias 名称应该与 actions.xml 中操作的 alias 名称完全相同。在这种情况下,alias 名称是 CreateIssueDetails

  5. 创建操作类 com.jtricks.MyCreateIssueDetails

  6. 我们可以在 MyCreateIssueDetails 中完成完整的操作类实现。但在大多数情况下,你可能只需要覆盖现有操作类的一些方法,就像我们的例子一样。如果是这样,只需像这样扩展原始的操作类:

    public class MyCreateIssueDetails extends CreateIssueDetails{
    
  7. 添加适当的构造函数以执行依赖注入并调用父类构造函数。Eclipse 或你使用的 IDE 通常会提示这一点。如果你需要任何其他管理类来添加额外的逻辑,也要在构造函数中注入它们。

  8. 覆盖你想要的方法。在我们的示例中,我们需要进行额外的验证。让我们看看如何添加一个验证,检查当前用户是否是负责人!

    @Override
    protected void doValidation() {
        //Our custom validation here
        String assignee = getSingleValueFromParameters("assignee");
        if (assignee == null || !assignee.equals(getRemoteUser().getName())){
    addErrorMessage("Assignee is not the current user, got U!");
        }
    super.doValidation();
    }
    
  9. 在这里,我们检查当前用户是否是负责人,如果不是,则添加一个错误信息。

  10. 打包插件并部署它。

  11. 创建一个有和没有指定自己为负责人(assignee)的 issue,看看 JIRA 如何表现!

它是如何工作的……

扩展现有操作的关键是使用相同的 alias 名称,在你的 webwork 插件模块中。JIRA 会在 actions.xml 中注册所有操作,并且如果找到相同的 alias 名称,会用插件中的操作覆盖它们。

在这种情况下,JIRA 为 CreateIssueDetails 操作注册了类 com.jtricks.MyCreateIssueDetails,而不是原始的 issue.CreateIssueDetails 类。

另请参见

  • 将新的 webwork 操作添加到 JIRA 中

  • 在第一章中部署你的插件

第三章:与自定义字段一起工作

在本章中,我们将涵盖:

  • 编写一个简单的自定义字段

  • 自定义字段搜索器

  • 处理问题中的自定义字段

  • 编程自定义字段选项

  • 重写自定义字段的验证

  • 自定义变更日志值

  • 从一种自定义字段类型迁移到另一种

  • 使自定义字段可排序

  • 在父问题的子任务列上显示自定义字段

  • 来自 4.1.x 的用户和日期字段

  • 将自定义字段添加到通知邮件

  • 为自定义字段添加帮助文本

  • 从选择字段中删除“无”选项

  • 使自定义字段项目可导入

  • 改变文本区域自定义字段的大小

介绍

对于一个问题追踪应用程序,提供尽可能多的关于问题的细节会更好。JIRA 通过提供一些标准问题字段来帮助我们,这些字段在创建问题时最有可能被使用。但如果我们需要捕获额外的信息,比如报告人父亲的名字,或者一些其他值得捕获的内容,可能是 SLA 或预估的成本呢?为此,我们可以利用自定义字段。

JIRA 提供了一组预定义的自定义字段类型,包括数字字段、用户选择器等,这些是 JIRA 用户最可能使用的类型。但随着你成为 JIRA 的高级用户,你可能会遇到需要自定义字段类型的情况。此时,人们会开始编写自定义字段插件,以创建新的字段类型或自定义搜索器。

我们将利用本章来更深入了解自定义字段。

编写一个简单的自定义字段

在本例中,我们将看到如何编写一个新的自定义字段类型。创建后,我们可以在 JIRA 实例中创建多个此类型的自定义字段,然后可以用来捕获问题上的信息。

新的自定义字段类型是通过customfield-type模块创建的。以下是支持的主要属性和元素。

属性:

Name 描述
key 这应该在插件中是唯一的。
class 必须实现com.atlassian.jira.issue.customfields.CustomFieldType接口。
i18n-name-key 插件模块的人类可读名称的本地化键。
Name 网络资源的人类可读名称。

元素:

Name 描述
Description 自定义字段类型的描述。
resource type="velocity" 自定义字段视图的 Velocity 模板。

准备工作

在开始之前,创建一个骨架插件。接下来,使用骨架插件创建一个 Eclipse 项目,我们就可以开始了!

如何实现...

在本例中,让我们看一个示例自定义字段类型,以便更容易理解。我们可以考虑创建一个只读自定义字段,存储最后一次编辑问题的用户名称。它功能简单,足以解释基本概念。

以下是需要做的主要步骤:

  1. 修改atlassian-plugin.xml文件,包含customfield-type模块。确保添加了适当的类名和视图。

    对于我们的例子,修改后的atlassian-plugin.xml文件将如下所示:

    <customfield-type key="readonly-user" name="Read Only User CF" class="com.jtricks.ReadOnlyUserCF">       
      <description>Read Only User CF Description</description>       
      <resource type="velocity" name="view" location="templates/com/jtricks/view-readonly-user.vm" />       
      <resource type="velocity" name="column-view" location="templates/com/jtricks/view-readonly-user.vm" />      
      <resource type="velocity" name="xml" location="templates/com/jtricks/view-readonly-user.vm" />      
      <resource type="velocity" name="edit" location="templates/com/jtricks/edit-readonly-user.vm" /> 
    </customfield-type>
    
  2. 确保插件中的键是唯一的。

  3. 实现该类。如引言中所述,该类必须实现com.atlassian.jira.issue.customfields.CustomFieldType接口。现在,如果你这样做,确保实现接口中的所有方法。

    注意

    一种更简单的方法是重写一些现有的自定义字段实现,如果有类似于你正在开发的类型。在这种情况下,你只需要重写某些方法,或者可能只需要修改 velocity 模板!

    有关现有实现的详细信息,可以在CustomFieldType接口的 Javadocs 中找到。NumberCFTypeDateCFTypeUserCFType等是一些有用的示例。

    在我们的示例中,类是com.jtricks.ReadOnlyUserCF。现在,我们的字段类型实际上不过是一个文本字段,因此重写现有的TextCFType是有意义的。

    以下是该类的结构:

    public class ReadOnlyUserCF extends TextCFType{
        private final JiraAuthenticationContext authContext;
        public ReadOnlyUserCF(CustomFieldValuePersister 
          customFieldValuePersister,
          StringConverter stringConverter,  GenericConfigManager 
          genericConfigManager,
          JiraAuthenticationContext authContext) {
          super(customFieldValuePersister, stringConverter, 
          genericConfigManager);
          this.authContext = authContext;
        }
        // Overridden methods here
      }
    

    如你所见,类扩展了TextCFType类。我们执行了‘构造函数注入’来调用超类构造函数。你需要做的就是在类的公共构造函数中添加所需的组件作为参数,Spring 会在运行时注入该组件的实例。在这里,JiraAuthenticationContext被注入,因为我们在类中使用了它。如你所见,authContext是一个被注入的参数,并分配给具有相同名称的类变量,以便在后续的方法中使用。

  4. 实现/重写感兴趣的方法。如前所述,如果你直接实现接口,必须实现所有必需的方法。

    在我们的例子中,我们扩展了TextCFType,因此只需要重写选定的方法。

    我们在这里唯一重写的方法是getVelocityParameters方法,我们在其中用额外的值填充了 velocity 参数。在这个例子中,我们添加了当前用户的名字。稍后我们将在 velocity 上下文中使用这些参数来生成视图。相同的方法也用于在不同的场景中创建不同的视图,例如创建、编辑等等。以下是代码片段:

    @Override
    public Map getVelocityParameters(Issue issue, CustomField 
        field, FieldLayoutItem fieldLayoutItem){
        Map params = super.getVelocityParameters(issue, field, 
        fieldLayoutItem);
        params.put("currentUser", authContext.getUser().getName());
        return params;
    }
    

    注意:如果使用 JIRA 4.3+,请使用authContext.getLoggedInUser方法。

  5. 创建在atlassian-plugin.xml文件中定义的模板。模板可以根据你希望字段在不同场景中呈现的方式编写。

    如果仔细观察,我们定义了四个 velocity 资源,但只使用了两个 velocity 模板文件,因为view-readonly-user.vm在‘view’,‘column-view’和‘xml’资源中是共享的。在这个示例中,我们只需要在这三种情况中显示readonly字段,因此模板如下所示:

    #if ($value)  $value  #end
    

    这段代码使用了 Velocity 语法,相关的详细信息可以在 velocity.apache.org/engine/devel/developer-guide.html 找到。这里我们展示了问题的现有自定义字段值。

  6. 编辑模板应是一个 readonly textfield,其 id 为自定义字段的 ID,因为 JIRA 使用这个 ID 在问题编辑时将值存储回数据库。模板如下所示:

    <input type="text" name="$customField.id" value="$currentUser" id="$customField.id"   class="textfield" readonly="readonly" />
    

    在这里,我们使用字段 currentUser,正如我们在第 4 步中添加到 Velocity 上下文中的那样。文本字段的值是 $currentUser。还请注意,ID 是 $customfield.id,并且存在 readonly 属性,以使其为只读。

  7. 打包插件并部署它!

请记住,更多复杂的逻辑和美化可以加入到类和 Velocity 模板中。正如人们所说,天高任鸟飞!

它是如何工作的...

插件安装完成后,可以在 管理 | 问题字段 | 自定义字段 下找到它。

创建一个我们刚刚创建的类型的新自定义字段,并将其映射到适当的问题类型和项目中。还需要将字段添加到适当的屏幕上。完成后,该字段将在问题的适当位置可用。

更多有关添加自定义字段的详细信息可以在 confluence.atlassian.com/display/JIRA/Adding+a+Custom+Field 查找。

在我们的示例中,每当编辑一个问题时,编辑该问题的用户的姓名会存储在自定义字段中。

还有更多内容...

你可能已经注意到,我们只在 Velocity 上下文中添加了一个参数,即 currentUser,但我们在视图模板中使用了 $value。那么这个 value 变量是从哪里来的呢?

JIRA 除了我们新增的变量外,还会自动填充一些现有变量到自定义字段的 Velocity 上下文中。value 就是其中之一,完整的列表可以在 confluence.atlassian.com/display/JIRADEV/Custom+field+Velocity+context+unwrapped 查找。

你可能会注意到,authContext 已经在 Velocity 上下文中可用,因此我们本可以直接在 Velocity 模板中获取当前用户,而不是在类的构造函数中注入 JiraAuthenticationContext,并从中获取 currentUser 变量。但我们这么做仅仅是为了说明这个示例。

另请参见

  • 在 第一章 中 创建一个骨架插件插件开发流程

  • 在 第一章 中 部署你的插件

自定义字段搜索器

编写自定义字段类型是一回事,但使其可用于 JIRA 的强大功能之一——搜索——则是另一回事!在创建自定义字段时,你可以关联要与其一起使用的搜索器。

在大多数情况下,你无需自定义搜索器。相反,你可以使用 JIRA 本身内置的自定义字段搜索器。该列表包括但不限于:文本字段搜索器、日期搜索器、数字搜索器、用户搜索器等。

当然,第一步是确定你的新字段需要哪种类型的搜索器。例如,Select 字段可以通过 Text Searcher 或 Exact Text Searcher 轻松搜索!User Picker 字段可以使用 User Searcher 或 Text Searcher 进行搜索。你甚至可能想扩展这些搜索器中的某一个,加入一些额外的功能,比如一些特殊条件或黑客技巧!是的,你明白我的意思吧!

以下是 JIRA 为其系统自定义字段定义的文本搜索器:

<customfield-searcher key="textsearcher" name="Free Text Searcher"
        i18n-name-key="admin.customfield.searcher.textsearcher.name"
        class="com.atlassian.jira.issue.customfields.searchers.TextSearcher">
        <description key="admin.customfield.searcher.textsearcher.desc">Search for values using a free text search.</description>
        <resource type="velocity" name="search" location="templates/plugins/fields/edit-searcher/search-basictext.vm"/>
        <resource type="velocity" name="view" location="templates/plugins/fields/view-searcher/view-searcher-basictext.vm"/>
        <valid-customfield-type package="com.atlassian.jira.plugin.system.customfieldtypes" key="textfield"/>
        <valid-customfield-type package="com.atlassian.jira.plugin.system.customfieldtypes" key="textarea"/>
        <valid-customfield-type package="com.atlassian.jira.plugin.system.customfieldtypes" key="readonlyfield"/>
</customfield-searcher>

如你所见,它使用了 customfield-searcher 模块。应该通过 Free Text Searcher 搜索的自定义字段应该添加在 valid-customfield-type 标签下。

以下是 customfield-searcher 模块支持的关键属性和元素。

属性

名称 描述
key 这个值在插件中应该是唯一的
class 必须实现 com.atlassian.jira.issue.customfields.CustomFieldSearcher 接口
i18n-name-key 插件模块的本地化名称键
name 网络资源的人类可读名称

元素

名称 描述
description 自定义字段搜索器模块的描述
resource type="velocity" 自定义字段搜索器视图的 Velocity 模板
valid-customfield-type 定义此搜索器可以应用的自定义字段类型。它有两个属性:package – 自定义字段所在的 atlassian 插件的 key,key – 自定义字段类型的模块 key。

让我们详细看看如何为之前示例中编写的自定义字段定义一个搜索器。

准备工作

确保你已使用之前的示例创建了 只读用户 自定义字段(com.jtricks.ReadOnlyUserCF)。

如何实现...

和往常一样,我们将按步骤进行:

  1. customfield-searcher 模块添加到 atlassian-plugin.xml 文件中。

    在我们的示例中,字段是一个只读文本字段,用于存储用户名,因此使用现有的TextSearcher比编写一个新的搜索器类更合适。模块将如下所示:

    <customfield-searcher key="readonly-user-searcher" name="Read Only User Searcher"  class="com.atlassian.jira.issue.customfields.searchers.TextSearcher">
    
      <description key="admin.customfield.searcher.textsearcher.desc">Search for Read Only User using a free text search.</description>
      <resource type="velocity" name="search" location="templates/plugins/fields/edit-searcher/search-basictext.vm"/>
      <resource type="velocity" name="view" location="templates/plugins/fields/view-searcher/view-searcher-basictext.vm"/>
      <valid-customfield-type package="com.jtricks" key="readonly-user"/>  
    </customfield-searcher>
    

    在这里,我们使用了实现 com.atlassian.jira.issue.customfields.CustomFieldSearcher 接口的 com.atlassian.jira.issue.customfields.searchers.TextSearcher。如果我们需要编写自定义搜索器,合适的类应该出现在这里。

    我们还需要为 editview 场景定义 Velocity 模板。

  2. 实现自定义字段搜索器类。在这个案例中,我们可以跳过这一步,因为我们使用的是已经实现的类TextSearcher

    即使我们在实现自定义搜索器时,最好还是扩展一个已存在的搜索器类,并只覆盖感兴趣的方法,这样可以避免从头开始实现所有内容。话虽如此,完全由开发人员决定是否提供全新的实现。

    唯一需要注意的强制要求是,搜索器类必须实现com.atlassian.jira.issue.customfields.CustomFieldSearcher接口。

  3. 编写 Velocity 模板。对于自定义字段搜索器,有两个视图:Editview,它们都将在问题导航器中显示。

    edit模板在创建/编辑过滤器时使用。view模板在查看过滤器或通过点击问题导航器中的 查看和隐藏(JIRA 4.3 中的“搜索”)查看搜索结果时使用。

    在我们的示例中,我们使用了内置的 JIRA 模板,但完全可以根据需要自定义这些模板的实现。

  4. 确保正确输入valid-customfield-type标签。

    这里有一个基本但常见的错误。package属性指的是自定义字段所在的 atlassian 插件键,而不是 Search 类所在的 Java 包!为了明确起见,atlassian 插件键就是你在atlassian-plugin.xml文件中的第一行中的键,在我们这个例子中是com.jtricks

    <atlassian-plugin key="com.jtricks" name="J-Tricks Customfields Plugin"  plugins-version="2">
    

    该插件(插件键)以及自定义字段键(此处为readonly-user)将指向正确的自定义字段。这也意味着,你可以在另一个插件中使用相同的readonly-user,并且插件键不同!

  5. 打包插件并部署它。

它是如何工作的……

一旦自定义字段类型与搜索器通过customfield-searcher模块关联,你会看到它出现在搜索器下拉菜单中,当创建该类型的自定义字段时。

对于任何现有的自定义字段,可以通过编辑操作来定义或修改搜索器。一旦搜索器更改,必须重新索引才能使更改生效。

我们可以为单个搜索器定义多个自定义字段,使用valid-customfield-type元素。

同样,同一类型的自定义字段可以在多个搜索器下定义。这将非常有用,当两个不同的相同类型的自定义字段可能需要使用两个不同的搜索器时。例如,文本字段可以使用TextSearcherExactTextSearcher

一旦搜索器与自定义字段定义关联后,你可以在问题导航器中看到它 当选择正确的上下文时。最后这一部分非常重要,因为字段仅在选择的上下文正确时才会出现在搜索中。例如,如果字段 X 只在缺陷上可用,当选择的问题类型同时包含缺陷和新特性时,它将不会出现在问题导航器中。选择正确的上下文后刷新搜索菜单,即可查看你的字段。这仅适用于 简单 搜索。

还有更多……

随着 v2 插件的引入,得益于 OSGI 捆绑包,直接在atlassian-plugin.xml文件中引用内置的 JIRA 搜索器类有时会失败,因为它无法解析所有的依赖项(臭名昭著的依赖未满足错误!)。这是因为某些类在 v2 插件中无法进行依赖注入,而在 v1 插件中是可以的。

但是有一个简单的解决方法。只需创建一个虚拟的自定义搜索器类,并在构造函数中为你进行依赖注入:

public class MySearcher extends SomeJiraSearcher {
   public MySearcher(PluginComponent ioc) {
      super(ioc, ComponentManager.getInstanceOfType(anotherType));
  } 
}

如果那样不行,可以将字段添加到WEB-INF/classes下的system-customfield-types.xml文件中,与 JIRA 系统自定义字段一起,换句话说,在相关的customfield-searcher元素中再添加一个valid-customfield-type条目。如果你这么做,记得在 JIRA 升级时应用这个解决方法!

处理问题中的自定义字段

在这个教程中,我们将学习如何在问题上处理自定义字段。它涵盖了如何从问题中读取自定义字段值,然后在问题中更新自定义字段值,无论是否触发通知。

准备工作

确定需要操作自定义字段的位置,可以是在监听器、工作流元素中,或者我们插件中的其他地方。

如何做到这一点...

我们将学习如何访问自定义字段的值,并在过程中修改该值。

以下是从Issue对象中读取自定义字段值的步骤。

  1. 创建一个CustomFieldManager类的实例。这是执行大部分自定义字段操作的管理器类。有两种方式可以获取管理器类:

    • 在插件类实现的构造函数中注入管理器类。

    • 直接从ComponentManager类中获取CustomFieldManager。可以通过以下方式完成:

      CustomFieldManager customFieldManager = ComponentManager.getInstance().getCustomFieldManager();
      
  2. 使用customfield名称或 ID 来检索customField对象:

    CustomField customField = customFieldManager.getCustomFieldObject(new Long(10000)); 
    

    或者

    CustomField customField = customFieldManager.getCustomFieldObjectByName(demoFieldName);
    
  3. 一旦自定义字段对象可用,就可以通过以下方式检索与某个问题相关的自定义字段值:

    Object value = customField.getValue(Issue) 
    

    或者

    Object value = issue.getCustomFieldValue(customField);
    

    后者在所有场景下都能正常工作,而前者在工作流验证器和后置函数等情况下似乎会失败。

  4. 将值对象强制转换为适当的类。例如,文本字段用String,多选字段用List<String>,数字字段用Double,依此类推。

如果你想将自定义字段值更新回问题中,请继续以下步骤。

  1. 创建一个修改过的值对象,其中包含旧值和新值:

    ModifiedValue modifiedValue = new ModifiedValue(value, newValueObject);
    
  2. 获取与该问题相关联的自定义字段的FieldLayoutItem

    FieldLayoutManager fieldLayoutManager = ComponentManager.getInstance().getFieldLayoutManager();
    FieldLayoutItem fieldLayoutItem = fieldLayoutManager.getFieldLayout(issue).getFieldLayoutItem(customField);
    
  3. 使用fieldLayoutItemmodifiedValue和默认更改持有者来更新问题的自定义字段值:

    customField.updateValue(fieldLayoutItem, issue, modifiedValue, new DefaultIssueChangeHolder());
    

这样做的优点,或者根据你的看法是缺点,便是自定义字段的值更改不会触发通知。如果你希望触发通知,请按照以下步骤更新问题,而不是之前的步骤。

  1. 修改问题的自定义字段值:

    issue.setCustomFieldValue(customField, value);
    
  2. 使用issueObjectremoteUser等详细信息创建操作参数映射:

    Map actionParams = EasyMap.build("issue", getIssue(), "issueObject", getIssueObject(), "remoteUser", ComponentManager.getInstance().getJiraAuthenticationContext().getUser());
    
  3. 执行ISSUE_UPDATE操作:

    ActionResult aResult = CoreFactory.getActionDispatcher().execute(ActionNames.ISSUE_UPDATE, actionParams);
    

这将抛出一个问题更新事件,所有处理程序将能够接收到该事件。

更新自定义字段的一个替代方法,可能更简单,就是使用字段的类型,如下所示:

customField.getCustomFieldType().updateValue(customField, issue, newValue);

它是如何工作的...

当使用上述方法之一更改自定义字段值时,后端发生的事情如下。

  • 值会在数据库中更新

  • 会创建一个变更记录,并更新变更历史以反映最新的更改

  • 索引会被更新以保存新的值

  • 如果使用ActionDispatcher更新字段,则会触发一个问题更新事件,进而触发通知和监听器

另见

  • 在第九章中,使用自定义字段和 SOAP远程访问 JIRA

编程自定义字段选项

我们已经看到如何创建自定义字段类型,如何搜索它,并且如何从问题中读取/更新其值。但关于多值自定义字段的一个重要方面,我们还没有看到的是自定义字段选项。

对于多值自定义字段,管理员可以配置允许的值集,也称为选项。一旦选项被配置,用户只能在这些选项中选择值,并且会进行验证以确保这样做。

那么,如何通过编程方式读取这些选项或向自定义字段添加新选项,以便以后可以在问题上设置它呢?让我们在这个配方中看看。

准备工作

在你的 JIRA 实例中创建一个多值自定义字段,假设为 X,给字段 X 添加一些选项。

如何操作...

为了处理自定义字段选项,Atlassian 编写了一个名为OptionsManager的管理类。

以下是获取为自定义字段配置的选项的步骤:

  1. 获取OptionsManager类的实例。与任何其他管理类类似,这可以通过两种方式来完成。

    • 在构造函数中注入管理类

    • 如下所示,直接从ComponentManager类中获取实例:

      optionsManager = ComponentManager.getOSGiComponentInstanceOfType(OptionsManager.class);
      
  2. 检索自定义字段的字段配置方案。

    自定义字段可能有多个字段配置方案,每个方案都有自己的一组项目、问题类型等,定义在不同的上下文中。我们需要确定我们感兴趣的字段配置方案:

    List<FieldConfigScheme> schemes = fieldConfigSchemeManager.getConfigSchemesForField(customField);
    
  3. 从方案中检索字段配置:

    FieldConfig config = fieldConfigScheme.getOneAndOnlyConfig();
    
  4. 一旦字段配置可用,我们就可以使用它来检索该字段配置的自定义字段选项。不同的上下文可能会有不同的选项,这就是为什么我们先检索config并用它来获取选项的原因:

    Options options = this.optionsManager.getOptions(config);
    List<Option> existingOptions = options.getRootOptions();
    

    option.getValue() 在遍历前述列表时会返回选项的名称。

    option.getChildOptions() 将在级联选择或其他多级选择的情况下检索子选项。

如果你需要向列表中添加新选项,OptionsManager再次来帮助我们。我们可以按照以下方式操作:

  1. 创建新选项:

    Option option = this.optionsManager.createOption(fieldConfig, null, sequence, value);
    

    第一个参数是我们之前看到的fieldConfig。第二个参数是parent option ID,用于多级自定义字段(如级联选择)的情况。对于单级自定义字段,它将是null。第三个参数是sequence,它决定了选项显示的顺序。第四个参数是要作为选项添加的实际value

  2. 将新的option添加到选项列表中并更新!

    this.optionsManager.updateOptions(modifiedOptions);
    
  3. 删除和更新选项也可以像这样进行,但我们不能忘记处理那些已使用这些选项值的现有问题。

  4. OptionsManager提供了许多其他有用的方法来处理自定义字段选项,这些方法可以在 Javadocs 中找到。

另见

  • 编写一个简单的自定义字段

自定义字段的验证覆盖

我们已经看到如何编写一个自定义字段,并通过编程设置其选项。我们还讨论了如何验证多值自定义字段上设置的值是否符合其预配置选项。如果值不属于预配置选项中的任何一个,验证失败,问题将无法创建或更新。

但是,如果我们遇到一个需要抑制验证的场景怎么办?如果我们需要向一个问题添加一些不来自其预配置选项的值怎么办?通常,你会像之前所示那样通过编程将其添加到选项中,但如果因为某些原因我们不想这么做呢?这时,你可以在自定义字段中抑制验证。

准备就绪

创建你的自定义字段,就像我们在本章的第一个配方中看到的那样。

如何操作...

在这里你需要做的就是,如果你正在扩展一个现有的自定义字段类型(如MultiSelectCFType),抑制原始父自定义字段中发生的验证。以下是你应该覆盖的方法:

@Overridepublic void validateFromParams(CustomFieldParams arg0, ErrorCollection arg1, FieldConfig arg2) {
  // Suppress any validation here
}

你还可以在这个方法中添加任何额外的验证!

如果你从头开始编写一个自定义字段类型,你将实现CustomFieldType接口。然后你需要实现上述方法,并且可以执行相同的操作。

如果你有兴趣并且能够访问 JIRA 源代码,去看看现有自定义字段类型中是如何进行验证的吧!

另见

  • 编写一个简单的自定义字段

自定义变更日志值

在编写某些自定义字段类型时,我们可能会遇到一个场景,即操作如何显示变更日志。对于一个普通的版本选择器自定义字段,变更日志将如下所示。

自定义变更日志值

这里Test Version是字段名称。你看到的第一个值Test2 [10010]是旧值,第二个值Test1 [10000]是新值。

准备就绪

编写你的自定义字段类型,正如本章的第一个配方所描述的。

如何操作...

如你在前面的屏幕中所见,旧值和新值的变更日志值以以下格式显示:

change log string [change log id]

字符串值和 ID 值都存储在ChangeItem表中。但在将值存储到数据库之前,该值是通过单个自定义字段生成的。这正是我们需要拦截并操作变更日志写入方式的地方。

有两个方法,一个是change log string,另一个是change log id,需要修改。以下是接口中的方法定义:

public String getChangelogValue(CustomField field, Object value);
public String getChangelogString(CustomField field, Object value);

你只需要实现这些方法,或者如果你在扩展现有的自定义字段类型时,可以重写这些方法来加入自定义实现。

如果你不希望字符串出现在变更历史中,只需在getChangelogString方法中返回null。请注意,如果在getChangelogValue方法中返回null,则不会创建变更日志!

让我们考虑一个简单的例子,当字符串的长度超过 100 个字符时,变更历史字符串会被截断。在这种情况下,getChangelogValue返回空字符串,而getChangelogString返回截断后的字符串。重写后的方法如下所示:

@Override
public String getChangelogValue(CustomField field, Object value) {
  return "";
}

@Override
public String getChangelogString(CustomField field, Object value) {
  String val = (String) value;
  if (val != null && val.length() > 100){
    val = val.substring(0, 100) + "....";
  }
  return val;
}

它是如何工作的...

每当自定义字段的值发生变化时,它会在CustomFieldValue表中更新该值。此外,它还通过创建变更日志条目来存储该问题的变化。

每当某个问题在单次更新中发生变化时,都会在ChangeGroup表中创建一条记录。它存储了执行更改的用户姓名(作者)、更改时间(创建时间)和问题 ID(问题)。

对于每个变更组,都将在ChangeItem表中存储一个或多个变更项。在此表中存储了字段的旧值和新值。对于旧值和新值,表中有两列——一列用于字符串表示,另一列用于 ID。以下是ChangeItem表的实体定义:

<!-- entity to represent a single change to an issue. Always part of a change group -->
    <entity entity-name="ChangeItem" table-name="changeitem" package-name="">
        <field name="id" type="numeric"/>
        <field name="group" col-name="groupid" type="numeric"/>

        <!-- whether this is a built in field ('jira') or a custom field ('custom') - basically used to avoid naming scope problems -->
        <!-- also used for keeping record of the bug_id of a bug from Bugzilla Import-->
        <!-- and for keeping record of ids in issue move-->
        <field name="fieldtype" type="long-varchar"/>
        <field name="field" type="long-varchar"/>

 <field name="oldvalue" type="extremely-long"/>
 <!-- a string representation of the new value (i.e. "Documentation" instead of "4" for a component which might be deleted) -->
 <field name="oldstring" type="extremely-long"/>

 <field name="newvalue" type="extremely-long"/>
 <!-- a string representation of the new value -->
 <field name="newstring" type="extremely-long"/>
        <prim-key field="id"/>
        <!—relations and indexes -->
    </entity>

字段oldvaluenewvalue是通过getChangelogValue方法填充的。同样,字段oldstringnewstring是通过getChangelogString方法填充的。

这些字段是用于显示变更历史时的字段。

从一个自定义字段类型迁移到另一个自定义字段类型

你使用 JIRA 已经超过一年了吗?或者你是 JIRA 的高级用户吗?也就是说,你是否进行了大量的自定义,创建了众多插件,使用了很多用例,等等?那么你很可能遇到过这种场景:你想把旧自定义字段的值迁移到新字段中。

JIRA 没有标准的方式来执行这个操作。但你可以通过修改 JIRA 数据库来在一定程度上实现此操作。即使使用 SQL,也会有一些限制。

首先要检查的是两个字段是否兼容。在没有额外检查和验证的情况下,您无法将值从文本字段移动到数字字段。如果某个问题中存储的值为1234a,则无法将其存储为数字字段,因为它不是有效的数字。所有字段类型都适用同样的规则。

让我们看看兼容类型的迁移,并讨论一下这个教程中的其他一些场景。

如何操作...

假设您有两个文本字段Field AField B。我们需要将每个问题中的Field A的值迁移到Field B。以下是应该执行的步骤:

  1. 关闭 JIRA 实例。

  2. 备份数据库。如果发生任何问题,我们可以恢复到此备份。

  3. 连接到您的数据库:

  4. 执行以下 SQL 查询:

    Update customfieldvalue set customfield = (select id from customfield where cfname='Field B')  where customfield = (select id from customfield where cfname='Field A')
    

    查询假设自定义字段名称是唯一的。如果您有多个自定义字段具有相同的名称,请改用 ID。

  5. 提交更改。

  6. 断开与数据库的连接。

  7. 启动 JIRA。

  8. 通过转到管理 | 系统 | 索引来重新索引 JIRA。

就这样!请验证您在问题和过滤器中的更改。

注意

所有的 SQL 语句和数据库引用都基于 Oracle 10g。请根据您的数据库进行相应修改。

它是如何工作的...

我们在这里所做的仅仅是更改了customfieldvalue表中的自定义字段 ID。其他步骤是执行任何 SQL 语句时的标准操作。

记住,如果您有两个具有相同name的自定义字段,请确保使用正确的id,而不是通过 SQL 中的name来查找。

现在,如果两个字段是相同类型,这个方法将正常工作。但如果您想将值从一种类型迁移到另一种类型怎么办?这并非总是可能的,因为在customfieldvalue表中的某些值可能与其他自定义字段类型不兼容。

假设我们要将一个普通的文本字段迁移到文本区域自定义字段。文本区域自定义字段中的值作为CLOB存储在数据库中的textvalue列中。而普通文本字段中的值作为 VARCHAR2(255)存储在stringvalue列中。因此,在转换时,我们需要更新自定义字段 ID,从stringvalue列中读取 VARCHAR2(255)值,并将其作为CLOB存储到textvalue列中,并将不再使用的stringvalue设置为null,以释放数据库空间。

在这个示例中,如果您尝试逆向操作,即从文本区域迁移到文本字段,您应该考虑文本的长度,并删除多余的文本,因为文本字段最多只能容纳 255 个字符。

您可以通过查看getDatabaseType方法来查找各种自定义字段的数据类型。对于TextField,该方法如下所示:

protected PersistenceFieldType getDatabaseType()
{
  return PersistenceFieldType.TYPE_LIMITED_TEXT;
}

其他可用的字段类型包括TYPE_UNLIMITED_TEXT(例如,文本区域)、TYPE_DATE(日期自定义字段)和TYPE_DECIMAL(例如,数字字段)。

还有更多...

有时我们只需要更改自定义字段的类型,而不是创建一个新字段并迁移其值。让我们快速看看如何操作。

更改自定义字段的类型

在这种情况下,需要更新的表是CustomField表。我们需要做的只是更新customfieldtypekey。只需设置新的自定义字段类型键,格式为{YOUR_ATLASSIAN_PLUGIN_KEY}:{MODULE_KEY}。

对于文本字段,键值是com.atlassian.jira.plugin.system.customfieldtypes:textfield

对于不兼容的类型,我们需要考虑所有上述情况,并相应更新CustomFieldValue表。

参见

  • 从数据库中检索自定义字段详情

使自定义字段可排序

我们已经看到如何创建新的自定义字段,为它们编写新的搜索器,等等。与字段(无论是自定义字段还是标准 JIRA 字段)相关的另一个重要功能是将它们用于排序。但仅仅编写一个新的自定义字段类型并不会启用该字段的排序功能。

在本教程中,我们将看到如何启用自定义字段的排序功能。

准备工作

创建我们需要启用搜索的新的自定义字段类型。

如何操作...

这很容易做。你只需要做两个简单的步骤,确保自定义字段是可排序的字段:

  1. 实现SortableCustomField接口。一个新的自定义字段类型看起来应该像以下这样:

    public class DemoCFType extends AbstractCustomFieldType implements SortableCustomField
    

    如果你正在扩展一个现有的自定义字段类型,如TextCFType,它已经实现了该接口。

  2. 实现compare方法。以下是一个示例:

    public int compare(Object customFieldObjectValue1, Object customFieldObjectValue2, FieldConfig fieldConfig)
    {
      return new DemoComparator().compare(customFieldObjectValue1, customFieldObjectValue2);
    }
    

    这里的DemoComparator是一个自定义比较器,我们可以编写它来实现排序逻辑。

    如果不需要自定义比较器,只需调用SortableCustomField.compare()

它是如何工作的...

一旦自定义字段实现了SortableCustomField接口,我们可以点击问题导航器中的字段头部,看到它根据我们实现的逻辑进行排序。

还有更多...

BestNameComparatorFullNameComparatorLocaleComparatorGenericValueComparator等是一些随 JIRA 一起提供的可重用比较器。没有明确的列表,但如果你有访问权限,你会在 JIRA 源代码中找到很多它们。

参见

  • 编写一个简单的自定义字段

  • 使自定义字段可导入项目

在子任务列中显示自定义字段

这是你可以做的最简单的事情之一!但它有时会增加很大的价值。我们讨论的是在父问题页面上为子任务添加额外的列。

我们知道如何添加额外的字段,对吧?让我们来看看怎么做,特别是如何添加自定义字段。

如何操作...

简而言之,你需要在jira-application.properties文件中修改jira.table.cols.subtasks属性。以下是操作步骤。我们在这里展示了如何添加一个自定义字段的示例。

  1. 停止 JIRA。

  2. 导航到WEB-INF/classes文件夹,并修改jira-application.properties文件中的jira.table.cols.subtasks属性:

    jira.table.cols.subtasks = issuetype, status, assignee, customfield_10140, progress
    
  3. 添加你想要的额外字段,并与现有的字段(如状态、指派人等)一起显示。要将自定义字段添加到列中,添加customfield_xxxxxx,其中xxxxx是自定义字段的唯一数字 ID。你可以从数据库中找到这个唯一 ID,或者当你将鼠标悬停在自定义字段的任何操作上时(例如编辑),URL 中会显示该 ID。

  4. 启动 JIRA。

从 JIRA 4.4 开始,此属性可以在管理|常规配置|高级下找到。无需修改属性文件并重启 JIRA。

它是如何工作的...

JIRA 通过查看前面的属性在查看问题页面上呈现子任务列。虽然添加标准子任务字段是有用的,但有时添加自定义字段会极为有帮助。

在我们的示例中,我们添加了customfield_10140,其中10140是自定义字段的数字 ID。它存储与任务相关联的 URL,如下所示:

它是如何工作的...

看起来很有用,是吧?

4.1.x 版本的用户和日期字段

如果你从 4.1 之前的版本升级到 4.1 之后的版本,你一定注意到了新的查看问题页面。人们对新 UI 可用性有不同的看法,但有一点是每个人都同意的,那就是日期和用户字段在 UI 中的排列方式。你将看到用户和日期字段有自己独立的部分,如下图所示:

4.1.x 版本的用户和日期字段

那么我们的字段如何出现在这个部分呢?

如何实现...

当你编写新的日期字段或用户字段时,确保它出现在正确的部分,只需实现正确的接口即可!

对于用户字段,新的自定义字段类型类应该实现以下接口:

com.atlassian.jira.issue.fields.UserField

对于日期字段,实施以下操作:

com.atlassian.jira.issue.fields.DateField

如果你正在扩展现有的日期字段或用户字段,它们已经实现了接口,因此它们会自动出现在这里!

如果你不希望你的字段出现在特殊的日期/用户部分呢?只需忽略这些接口。字段将像普通的自定义字段一样显示,并按照字段配置中指定的顺序显示。

它是如何工作的...

这非常简单。JIRA 会查找实现了UserField/DateField接口的类,并将它们显示在相应的部分。在标准自定义字段部分,它不会显示这些字段。

曾经想过这个检查是如何在 JIRA 源代码中完成的吗?视图是在ViewIssue类中渲染的,但实际的检查是在util类中完成的:com.atlassian.jira.issue.fields.util.FieldPredicates

另见

  • 编写一个简单的自定义字段

向通知邮件中添加自定义字段

JIRA 的一个主要功能是其向选定人员发送通知的能力——针对特定事件发送通知!JIRA 用户常常要求定制这些通知,主要是添加更多的内容,形式为自定义字段。

如果你理解 velocity 模板,向通知邮件中添加自定义字段是轻松的事情,正如我们将在这个方案中看到的那样。

准备工作

你应该知道需要添加到模板中的自定义字段 ID。id 可以在你将鼠标悬停在管理页面上的“编辑”操作时的 URL 中找到。

如何操作...

让我们看一下在问题更新时如何将自定义字段 X 添加到通知邮件中。以下是步骤:

  1. 确定需要更新的模板。对于 JIRA 中的每个事件,可以在位于 WEB-INF/classes 文件夹下的 email-template-id-mappings.xml 文件中找到与之关联的模板。

    在这种情况下,事件是 问题已更新,匹配的模板是 issueupdated.vm

    一旦识别出模板,文件将位于 WEB-INF/classes/templates/email/text/WEB-INF/classes/templates/email/html/ 文件夹下。

  2. 修改模板,确保在需要的地方包含自定义字段的名称和值。

    可以通过以下方式检索自定义字段的名称:

    $customFieldManager.getCustomFieldObject("customfield_10010").getName()
    

    实际的值可以通过以下方式检索:

    $issue.getCustomFieldValue($customFieldManager.getCustomFieldObject("customfield_10010")))
    

    在这两种情况下,10010 是我们之前讨论的 customfield 的数字 ID。

它是如何工作的...

电子邮件通知是使用 velocity 模板渲染的。JIRA 在 velocity 上下文中已经有很多对象,包括我们刚刚使用的 customFieldManagerissue 对象。有关电子邮件模板的 velocity 上下文中可用的对象的完整列表,可以在 Atlassian 文档中找到,网址为 confluence.atlassian.com/display/JIRADEV/Velocity+Context+for+Email+Templates

在这种情况下,我们使用 customFieldManager 对象来检索自定义字段的信息,然后使用 issue 对象从问题中获取其值。

为自定义字段添加帮助文本

随着我们的 JIRA 实例的增长,并且通过自定义字段要求用户提供越来越多的信息,让用户了解我们期望他们提供什么,已经成为常态。除了我们可以为他们准备的一堆教程之外,在屏幕上、字段旁边提供帮助也很有意义。

让我们看看如何做的各种选项。

准备工作

确保你已经有了需要显示帮助的自定义字段,并且已正确配置。

如何操作...

提供帮助的方式有多种。让我们看看最广泛接受的几种方式:

  1. 链接到帮助页面。

    这只是常识。只需链接到某个地方托管的有关该字段的文档即可。我们可以通过在自定义字段的描述中添加几个超链接轻松实现这一点。我们只需要重用一些 JIRA 样式,以确保帮助内容在系统中的一致性。

    超链接可以通过两种方式添加。它们如下:

    1. 在新窗口中打开帮助文档

      这里我们只是链接到一个外部页面,该页面将在新窗口中打开。

      My Demo Field <a class="localHelp" href="http://www.j-tricks.com" title="Get My Help" target="_blank"><img src="img/help_blue.gif"</a>
      

      My Demo Field 这里是自定义字段名称。如你所见,我们使用了与 JIRA 一同提供的图像,以确保前面提到的一致性。这里需要注意的一点是图像的 URL —— /jira/images/icons/help_blue.gif。在这种情况下,我们假设 /jira 是该实例的上下文路径。如果没有上下文路径,只需使用 /images/icons/help_blue.gif 或者使用你实例的上下文路径替换 /jira

      还要注意 CSS 类 localHelp,它再次被用来确保所有帮助文本的一致性。根据需要修改帮助 URL 和标题。

    2. 以弹出窗口的形式打开帮助文档

      在这里,我们将帮助文档作为弹出窗口打开,而不是打开新窗口。焦点会转移到新窗口。

      My Demo Field <a class="localHelp" href="http://www.j-tricks.com" onclick="var child = window.open('http://www.google.com', 'myHelp', 'width=600, height=500, resizable, scrollbars=yes'); child.focus(); return false;"><img src="img/help_blue.gif" title="Get My Help "></a>
      

      再次强调,图像和 CSS 文件保持不变。这里我们可以指定弹出窗口的宽度、高度等,正如前面代码所示。其他一切保持不变!

  2. 提供内联帮助。

    如果帮助内容不够大,无法放入文档中,但同时你又不希望它与字段描述一起显示,这种情况适用!在这种情况下,我们使用一个小的 JavaScript 技巧,将帮助文本隐藏在一个 HTML DIV 中,并在用户点击帮助图片时切换其可见性。

    在修改相关文本后,将以下内容放置在字段描述下方。这里,My Demo Field 是实际的字段描述,而 Inline help for my demo field! 是我们添加的额外帮助:

    My Demo Field<a class="localHelp" href="#"  onclick=" AJS.$('#mdfFieldHelp').toggle();"><img src="img/help_blue.gif"</a>
    <div id="mdfFieldHelp" style="display:none">
    Inline help for my demo field!
    </div>
    

简短而直接,是吧?

它是如何工作的……

幸运的是,JIRA 允许在描述字段中渲染 HTML。我们仅仅利用了 HTML 功能为字段提供帮助。这为我们提供了很多选项,前面提到的仅仅是如何利用它的一些提示。

从 select 字段中移除 'none' 选项

如果你是 JIRA 插件开发人员,你一定遇到过这个功能请求。有些人就是不喜欢 select 字段中的 'none' 选项,原因各异。显而易见的原因之一是强制用户选择有效值。

如何操作……

Select Field 是一个系统自定义字段,使用 velocity 模板来渲染视图和编辑屏幕。为了移除 none 选项,我们需要修改编辑模板。

对于任何系统自定义字段,你可以在 WEB-INF/classes 文件夹下的 system-customfieldtypes-plugin.xml 文件中找到相关类及其 velocity 模板。

在我们的例子中,我们可以找到与 select-field 相关的以下代码片段:

<customfield-type key="select" name="Select List"
        i18n-name-key="admin.customfield.type.select.name"
        class="com.atlassian.jira.issue.customfields.impl.SelectCFType">
        <description key="admin.customfield.type.select.desc">A single select list with a configurable list of options.</description>
        <resource type="velocity" name="view" location="templates/plugins/fields/view/view-rawtext.vm"/>
        <resource type="velocity" name="edit" location="templates/plugins/fields/edit/edit-select.vm"/>
        <resource type="velocity" name="xml" location="templates/plugins/fields/xml/xml-basictext.vm"/>
    </customfield-type>

从前面的代码片段可以看出,select 字段的编辑模板是 templates/plugins/fields/edit/edit-select.vm。我们需要修改的文件就是这个。

我们现在需要做的就是导航到文件并删除以下几行:

#if (!$fieldLayoutItem || $fieldLayoutItem.required == false)
   <option value="-1">$i18n.getText("common.words.none")</option>
   #else
   <option value="">$i18n.getText("common.words.none")</option>
   #end

模板中的其余代码 不能 删除。

重启 JIRA 以使更改生效。

注意

同样的方法也可以用来移除其他字段的none选项,比如“单选按钮”、“多选框”、“级联选择”等等。移除的实际代码会有所不同,但方法是相同的。

还有更多…

还有更多内容…

在不重启的情况下重新加载 velocity 更改(自动重新加载)

你可以配置 JIRA 在不重启的情况下重新加载 velocity 模板的更改。为此,你需要对WEB-INF/classes中的velocity.properties文件进行两项更改:

  1. class.resource.loader.cache 属性设置为 false。默认情况下,它是 true

  2. 取消注释 velocimacro.library.autoreload=true 属性。这可以通过去掉行首的 # 来实现。

重启 JIRA 后,velocity 模板的更改将会被重新加载,无需再次重启!

另见

  • 更改文本区域自定义字段的大小

使自定义字段项目可导入

从 JIRA 3.13 开始,可以从现有的 JIRA 备份文件中导入单个项目。有关更多信息,请访问 confluence.atlassian.com/display/JIRA/Restoring+a+Project+from+Backup

在导入项目时,JIRA 允许你复制所有的任务数据,但只有在被要求时才会这么做!让我们看看如何使自定义字段的项目可导入,或者用简单的话来说,就是告诉 JIRA 我们的字段可以导入!

如何实现...

我们只需要实现以下接口,就能将自定义字段项目标记为可导入:com.atlassian.jira.imports.project.customfield.ProjectImportableCustomField

然后你需要实现以下方法:

ProjectCustomFieldImporter getProjectImporter();

已经有一些现有的实现,如 SelectCustomFieldImporter 类,供我们复用。在这个类中,我们会检查正在导入的值是否是有效值。

例如,对于一个 select 字段,我们需要确保导入的值是目标系统中自定义字段配置的有效选项。此时,具体的规则实现完全由用户决定。

查看 Javadocs:docs.atlassian.com/jira/latest/com/atlassian/jira/imports/project/customfield/ProjectCustomFieldImporter.html,获取有关自定义 ProjectCustomFieldImporter 实现的更多详细信息。

另见

  • 使自定义字段可排序

更改文本区域自定义字段的大小

正如我们之前讨论过的,JIRA 自带了一些预定义的自定义字段类型。常用的一种是文本区域字段。

文本区域字段有一个预定义的宽度和高度,不能自定义。JIRA 用户通常要求增加字段的大小,无论是全局设置还是为特定的自定义字段设置。

我们将在这篇教程中查看如何实现这一点。

如何做...

和其他自定义字段一样,文本区域字段也是通过 velocity 模板渲染的。从 system-customfieldtypes-plugin.xml 文件中,我们可以找到编辑模板的位置是 templates/plugins/fields/edit/edit-textarea.vm

<customfield-type key="textarea" name="Free Text Field (unlimited text)"
    ............................................
    <resource type="velocity" name="edit" location="templates/plugins/
    fields/edit/edit-textarea.vm"/>
    ...................................
</customfield-type>

如果我们需要增加大小,我们需要修改模板以增加 rowscols 属性,以满足要求。

如果我们需要将宽度(列数)增加到 50,高度(行数)增加到 8,则需要将 colsrows 属性分别更新为 50 和 8。模板将如下所示:

#controlHeader ($action $customField.id $customField.name $fieldLayoutItem.required $displayParameters.noHeader)

#if ($!customField.isRenderable() && $rendererDescriptor)

  ## setup some additional parameters
  $!rendererParams.put("rows", "8")
  $!rendererParams.put("cols", "50")
  $!rendererParams.put("wrap", "virtual")

  ## let the renderer display the edit component
  $rendererDescriptor.getEditVM($!value, $!issue.key, $!fieldLayoutItem.rendererType, $!customField.id, $!customField.name, $rendererParams, false)
#else
  <textarea name="$customField.id"
            id="$customField.id"
            class="textfield"
            rows="8" cols="50" wrap="virtual"
  >$textutils.htmlEncode($!value)</textarea>
#end

#controlFooter ($action $fieldLayoutItem.fieldDescription $displayParameters.noHeader)

如果这只需要为特定的 customfield 执行,只需在模板的开始部分添加一个条件,以便单独处理该自定义字段。模板将如下所示:

#controlHeader ($action $customField.id $customField.name $fieldLayoutItem.required $displayParameters.noHeader)

#if ($!customField.id=="customfield_10010")
  ## Modify rows and cols only for this custom field
  $!rendererParams.put("rows", "8")
  $!rendererParams.put("cols", "50")
  $!rendererParams.put("wrap", "virtual")

  ## let the renderer display the edit component
  $rendererDescriptor.getEditVM($!value, $!issue.key, $!fieldLayoutItem.rendererType, $!customField.id, $!customField.name, $rendererParams, false)

#elseif ($!customField.isRenderable() && $rendererDescriptor)
 // reminder of the above snippet here
.................................................................

#controlFooter ($action $fieldLayoutItem.fieldDescription $displayParameters.noHeader)

希望这能给你一些关于增加文本区域自定义字段大小的思路。

像往常一样,JIRA 需要重启才能使此更改生效,除非启用了 velocity 自动加载,正如我们在前面的教程中讨论的那样。

另请参见

  • 从选择字段中移除 'none' 选项

第四章:编程工作流

在本章中,我们将讨论:

  • 编写工作流条件

  • 编写工作流验证器

  • 编写工作流后置功能

  • 编辑活动中的工作流

  • 根据工作流状态使问题可编辑/不可编辑

  • 包括/排除特定转换的解析

  • 基于工作流状态的权限

  • 工作流转换中的国际化

  • 程序化地获取可用的工作流操作

  • 程序化地推进工作流

  • 从数据库获取工作流历史

  • 在 JIRA 中重新排序工作流操作

  • 在工作流中创建公共转换

  • Jelly 升级

介绍

工作流是一个突出的功能,它帮助用户将 JIRA 转变为一个更易用的系统。它帮助用户根据问题类型、使用 JIRA 的目的等来定义问题的生命周期。正如 Atlassian 文档中所说的,见 confluence.atlassian.com/display/JIRA/Configuring+Workflow

JIRA 工作流是一个问题在其生命周期中经历的步骤和转换的集合。工作流通常代表业务流程。

JIRA 使用 Opensymphony 的 OSWorkflow,这是一个高度可配置且更重要的是可插拔的工具,能够满足各种需求。JIRA 使用三个不同的插件模块来向工作流添加额外功能,我们将在本章中详细讨论。

为了简化操作,JIRA 提供了一个默认的工作流。我们不能修改默认工作流,但可以将其复制到新的工作流中并进行修改以满足我们的需求。在深入开发工作流之前,理解工作流的各种组件是有意义的。

JIRA 工作流中最重要的两个组件是步骤转换。在任何时候,问题都将处于某个步骤中。工作流中的每个步骤都与工作流状态(confluence.atlassian.com/display/JIRA/Defining+%27Status%27+Field+Values)相关联,这个状态你将在每个阶段看到的问题上显示。另一方面,转换是两个步骤之间的链接。它允许用户将问题从一个步骤移动到另一个步骤(本质上将问题从一个状态移动到另一个状态)。

关于工作流需要记住或理解的几点关键事项:

  • 一个问题在任何时候只能存在于一个步骤中

  • 一个状态只能映射到工作流中的一个步骤

  • 转换总是单向的。因此,如果需要返回到上一步,则需要一个不同的转换

  • 转换可以选择指定一个屏幕,向用户展示正确的字段

OSWorkflow,因此 JIRA,提供了将各种元素添加到工作流转换中的选项,简要总结如下:

  • 条件:一组在用户能够在问题上实际看到工作流操作(转换)之前需要满足的条件。

  • 验证器:一组验证器,用于在执行目标步骤之前验证用户输入。

  • 后置功能:一组在问题成功移动到目标步骤后执行的操作。

这三个元素赋予我们在问题从一个状态迁移到另一个状态时处理各种用例的灵活性。JIRA 提供了几个内置的条件、验证器和后置功能。也有许多插件提供了各种有用的工作流元素。如果你仍然找不到你需要的,JIRA 允许我们将其作为插件编写。我们将在本章中的多个实例中看到如何做到这一点。

希望这能让你对各种工作流元素有一个大致的了解。有关 JIRA 工作流的更多内容,可以在 JIRA 文档中找到,网址是 confluence.atlassian.com/display/JIRA/Configuring+Workflow

编写工作流条件

什么是工作流条件?它们决定了一个工作流操作是否可用。考虑到工作流在安装中的重要性以及需要根据某些标准(例如,字段不能为空!)或将操作限制为特定人员、角色等的需求,编写工作流条件是不可避免的。

工作流条件是通过 workflow-condition 模块创建的。以下是支持的关键属性和元素。有关更多详细信息,请参见 confluence.atlassian.com/display/JIRADEV/Workflow+Plugin+Modules#WorkflowPluginModules-Conditions

属性

Name 描述
key 该值在插件中应该是唯一的。
class 提供渲染的 Velocity 模板上下文的类。必须实现 com.atlassian.jira.plugin.workflow.WorkflowPluginConditionFactory 接口。
i18n-name-key 插件模块的可本地化名称的键。
name 工作流条件的可读名称。

元素

Name 描述
description 工作流条件的描述。
condition-class 用于确定用户是否能看到工作流转换的类。必须实现 com.opensymphony.workflow.Condition。推荐继承 com.atlassian.jira.workflow.condition.AbstractJiraCondition 类。
resource type="velocity" 工作流条件视图的 Velocity 模板。

准备工作

一如既往,创建一个骨架插件。使用骨架插件创建一个 Eclipse 项目,我们就可以开始了!

如何实现...

在这个示例中,假设我们要开发一个工作流条件,只允许属于特定项目角色的用户执行某个过渡。以下是编写条件的步骤:

  1. 定义配置工作流条件所需的输入项。

    我们需要实现WorkflowPluginFactory接口,该接口主要用于向模板提供速度参数。它将用于提取定义条件时所需的输入参数。为了明确,这里的输入项不是执行工作流操作时的输入,而是定义条件时的输入。

    条件工厂类,在此案例中为RoleConditionFactory,继承自AbstractWorkflowPluginFactory,并实现了WorkflowPluginFactory接口。我们需要实现三个抽象方法,分别是getVelocityParamsForInputgetVelocityParamsForEditgetVelocityParamsForView。顾名思义,它们用于填充不同场景下的速度参数。

    在我们的示例中,我们需要将工作流操作限制为特定项目角色,因此在定义条件时,我们需要选择项目角色。以下是三种方法的实现:

    private static final String ROLE_NAME = "role";
    private static final String ROLES = "roles";
    ………….
    @Override
    protected void getVelocityParamsForEdit(Map<String, Object> velocityParams, AbstractDescriptor descriptor) {
        velocityParams.put(ROLE, getRole(descriptor));
        velocityParams.put(ROLES, getProjectRoles());
    }
    
      @Override
      protected void getVelocityParamsForInput(Map<String, Object> velocityParams) {
        velocityParams.put(ROLES, getProjectRoles());
      }
    
      @Override
      protected void getVelocityParamsForView(Map<String, Object> velocityParams, AbstractDescriptor descriptor) {
        velocityParams.put(ROLE, getRole(descriptor));
      }
    

    让我们详细看看这些方法:

    • getVelocityParamsForInput:此方法定义了输入场景的速度参数,即用户初次配置工作流时的参数。在我们的示例中,我们需要展示所有项目角色,以便用户可以选择一个来定义条件。方法getProjectRoles仅返回所有项目角色,然后将角色集合以ROLES键放入速度参数中。

    • getVelocityParamsForView:此方法定义了视图场景的速度参数,即用户在配置后如何查看条件。在我们的示例中,我们定义了一个角色,因此在从工作流描述符中提取后,我们应该将其展示给用户。如果你注意到,描述符是AbstractDescriptor的一个实例,并作为方法的参数提供。我们只需从描述符中提取角色,方法如下所示:

      private ProjectRole getRole(AbstractDescriptor descriptor){
          if (!(descriptor instanceof ConditionDescriptor)) {
            throw new IllegalArgumentException("Descriptor must be a ConditionDescriptor.");
          }
      
          ConditionDescriptor functionDescriptor = (ConditionDescriptor) descriptor;
      
          String role = (String) functionDescriptor.getArgs().get(ROLE);
          if (role!=null && role.trim().length()>0)
            return getProjectRole(role);
          else 
            return null;
      }
      

      只需检查描述符是否为条件描述符,然后按前面的代码片段提取角色。

    • getVelocityParamsForEdit:此方法定义了编辑场景的速度参数,即用户修改现有条件时的参数。在这里,我们需要选项和已选值。因此,我们将项目角色集合和选中的角色都放入速度参数中。

  2. 第二步是为上述三种场景(输入视图编辑)定义速度模板。我们可以在这里为输入和编辑场景使用相同的模板,并通过简单的检查保持旧角色在编辑场景中的选中状态。让我们看看这些模板:

    • edit-roleCondition.vm:显示所有项目角色,并在编辑模式中突出显示已选中的角色。在输入模式中,使用相同的模板,但所选角色将为 null,因此需要进行 null 检查:

      <tr bgcolor="#ffffff">
          <td align="right" valign="top" bgcolor="#fffff0">
              <span class="label">Project Role:</span>
          </td>
          <td bgcolor="#ffffff" nowrap>
              <select name="role" id="role">
              #foreach ($field in $roles)
                <option value="${field.id}"
                  #if ($role && (${field.id}==${role.id}))
                      SELECTED
                  #end
                  >$field.name</option>
              #end
              </select>
              <br><font size="1">Select the role in which the user should be present!</font>
          </td>
                 </tr>
      
    • view-roleCondition.vm:显示所选的角色:

      #if ($role)
        User should have ${role.name} Role!
      #else
        Role Not Defined
      #end
      
  3. 第三步是编写实际的条件。条件类应扩展AbstractJiraCondition类。在这里,我们需要实现passesCondition方法。在我们的例子中,我们从议题中检索项目,检查用户是否具有适当的项目角色,如果用户有,返回 true:

    public boolean passesCondition(Map transientVars, Map args, PropertySet ps) throws WorkflowException {
        Issue issue = getIssue(transientVars);
        User user = getCaller(transientVars, args);
    
        project project = issue.getProjectObject();
        String role = (String)args.get(ROLE);
        Long roleId = new Long(role);
    
        return projectRoleManager.isUserInProjectRole(user, projectRoleManager.getProjectRole(roleId), project);
    }
    

    可以使用在AbstractJiraCondition类中实现的getIssue方法来获取检查条件的议题。同样,可以使用getCaller方法来获取用户。在前面的这个方法中,projectRoleManager是在构造函数中注入的,正如我们之前所见。

  4. 我们可以看到,ROLE键被用来从args参数中检索项目角色 ID,在passesCondition方法中使用。为了使ROLE键能够在args映射中使用,我们需要在条件工厂类中重写getDescriptorParams方法,在这个例子中是RoleConditionFactory类。getDescriptorParams方法返回一个已清理的参数映射,这些参数将通过 velocity 提交的数组形式的值传递给工作流插件实例,该数组包含插件配置页面中的一组name:value参数(即“input-parameters”velocity 模板)。在我们的案例中,该方法被重写如下:

    public Map<String, String> getDescriptorParams(Map<String, Object> conditionParams) {
        if (conditionParams != null && conditionParams.containsKey(ROLE))
            {
                return EasyMap.build(ROLE, extractSingleParam(conditionParams, ROLE));
            }
            // Create a 'hard coded' parameter
            return EasyMap.build();
      }
    

    这里的方法构建了一个key:value对的映射,其中 key 是ROLE,值是输入配置页面中输入的角色值。extractSingleParam方法在AbstractWorkflowPluginFactory类中实现。如果有多个参数需要提取,可以使用extractMultipleParams方法!

  5. 现在只剩下将上述组件填充到atlassian-plugin.xml文件中。我们使用workflow-condition模块,代码块如下:

    <workflow-condition key="role-condition" name="Role Based Condition"  class="com.jtricks.RoleConditionFactory">
        <description>Role Based Workflow Condition</description>
        <condition-class>com.jtricks.RoleCondition</condition-class>
        <resource type="velocity" name="view"  location="templates/com/jtricks/view-roleCondition.vm"/>
        <resource type="velocity" name="input-parameters" location="templates/com/jtricks/edit-roleCondition.vm"/>
        <resource type="velocity" name="edit-parameters"  location="templates/com/jtricks/edit-roleCondition.vm"/>
    </workflow-condition>
    
  6. 打包插件并部署!

工作原理...

插件部署后,我们需要修改工作流以包含该条件。以下截图展示了最初添加条件时的样子。正如你现在所知道的,这是使用输入模板呈现的:

How it works...

条件添加后(即选择了开发人员角色后),视图将使用视图模板渲染,如下图所示:

How it works...

如果你尝试编辑它,屏幕将使用编辑模板渲染,如下图所示:

How it works...

请注意,开发人员角色已经被选择。

在工作流配置完成后,当用户访问某个问题时,只有在该用户是问题所属项目角色的成员时,才会显示过渡操作。在查看问题时,condition类中的passesCondition方法将被执行。

另见

  • 在第一章中创建骨架插件插件开发过程

  • 在第一章中部署你的插件

编写工作流验证器

工作流验证器是特定的验证器,用于检查在工作流过程中是否满足某些预定义的约束条件。这些约束在工作流中进行配置,如果未满足某些条件,用户将收到错误提示。一个典型的例子是在问题被移至不同状态之前,检查某个特定字段是否存在。

工作流验证器是通过workflow-validator模块创建的。以下是支持的关键属性和元素。

属性

名称 描述
key 该值应在插件内唯一。
class 提供渲染的 Velocity 模板上下文的类。必须实现com.atlassian.jira.plugin.workflow.WorkflowPluginValidatorFactory接口。
i18n-name-key 插件模块的可本地化名称的键。
name 工作流验证器的可读名称。

元素

名称 描述
description 工作流验证器的描述。
validator-class 执行验证的类。必须实现com.opensymphony.workflow.Validator接口。
resource type="velocity" 用于工作流验证器视图的 Velocity 模板。

注意

查看confluence.atlassian.com/display/JIRADEV/Workflow+Plugin+Modules#WorkflowPluginModules-Validators了解更多详细信息。

准备工作

一如既往,创建一个骨架插件。使用骨架插件创建一个 eclipse 项目,然后我们就可以开始了!

如何操作...

假设我们要编写一个验证器,检查问题上是否已填写某个特定字段!我们可以通过以下步骤来完成:

  1. 定义配置工作流验证器所需的输入:

    我们需要实现WorkflowPluginValidatorFactory接口,主要是为了向模板提供 Velocity 参数。它将用于提取定义验证器时使用的输入参数。为了明确一点,这里的输入并不是执行工作流操作时的输入,而是定义验证器时的输入。

    验证器工厂类,在此案例中是FieldValidatorFactory,扩展了AbstractWorkflowPluginFactory接口,并实现了WorkflowPluginValidatorFactory接口。与条件类似,这里有三个我们需要实现的抽象方法。它们分别是getVelocityParamsForInputgetVelocityParamsForEditgetVelocityParamsForView。正如名称所示,它们用于在不同场景中填充速度参数。

    在我们的示例中,我们有一个单独的输入字段,即自定义字段的名称。这三个方法将如下实现:

    @Override
    protected void getVelocityParamsForEdit(Map velocityParams, AbstractDescriptor descriptor) {
      velocityParams.put(FIELD_NAME, getFieldName(descriptor));
      velocityParams.put(FIELDS, getCFFields());
    }
    
    @Override
    protected void getVelocityParamsForInput(Map velocityParams) {
        velocityParams.put(FIELDS, getCFFields());
    }
    
    @Override
    protected void getVelocityParamsForView(Map velocityParams, AbstractDescriptor descriptor) {
        velocityParams.put(FIELD_NAME, getFieldName(descriptor));
    }
    

    你可能已经注意到,这些方法与工作流条件中的方法非常相似,除了业务逻辑不同!让我们详细看看这些方法:

    • getVelocityParamsForInput:此方法定义了输入场景的速度参数,也就是用户初次配置工作流时的场景。在我们的示例中,我们需要显示所有的自定义字段,以便用户选择一个用于验证器。方法getCFFields返回所有自定义字段,然后将字段集合放入速度参数中,键为 fields。

    • getVelocityParamsForView:此方法定义了查看场景的速度参数,也就是用户在配置验证器后看到的内容。在我们的示例中,我们已经定义了一个字段,因此我们应该在从工作流描述符中检索回来后将其展示给用户。你可能已经注意到,描述符AbstractDescriptor的实例作为方法中的参数提供。我们需要做的就是从描述符中提取字段名称,方法如下:

      private String getFieldName(AbstractDescriptor descriptor){
        if (!(descriptor instanceof ValidatorDescriptor)) {
          throw new IllegalArgumentException('Descriptor must be a ValidatorDescriptor.');
        }
      
        ValidatorDescriptor validatorDescriptor = (ValidatorDescriptor) descriptor;
      
        String field = (String) validatorDescriptor.getArgs().get(FIELD_NAME);
        if (field != null && field.trim().length() > 0)
          return field;
        else
          return NOT_DEFINED;
      }
      

    只需检查描述符是否是验证器描述符,然后像前面示例所示提取字段即可。

    • getVelocityParamsForEdit:此方法定义了编辑场景的速度参数,也就是当用户修改现有验证器时的场景。这里我们需要既有选项,也有选中的值。因此,我们将自定义字段的集合和字段名称都放入速度参数中。
  2. 第二步是为上述三个场景定义速度模板,分别是输入、查看和编辑。我们可以在这里对输入和编辑使用相同的模板,只需进行简单检查,以确保在编辑场景中保持选中的旧字段。让我们来看一下这个模板:

    • edit-fieldValidator.vm:在编辑模式下显示所有自定义字段,并高亮显示已选中的字段。在输入模式下,字段变量为 null,因此没有任何字段被预选中:

      <tr bgcolor="#ffffff">
        <td align="right" valign="top" bgcolor="#fffff0">
          <span class="label">Custom Fields :</span>
        </td>
        <td bgcolor="#ffffff" nowrap>
          <select name="field" id="field">
          #foreach ($cf in $fields)
            <option value="$cf.name"
              #if ($cf.name.equals($field)) SELECTED #end
            >$cf.name</option>
          #end
          </select>
          <br><font size="1">Select the Custom Field to be validated for NULL</font>
        </td>
      </tr>
      
    • view-fieldValidator.vm:显示选中的字段:

      #if ($field)
        Field '$field' is Required!
      #end
      
  3. 第三步是编写实际的验证器。验证器类应该实现Validator接口。我们需要做的就是实现validate方法。在我们的示例中,我们从问题中获取自定义字段的值,如果该值为 null(空),则抛出InvalidInputException

    public void validate(Map transientVars, Map args, PropertySet ps) throws InvalidInputException, WorkflowException {
        Issue issue = (Issue) transientVars.get("issue");
        String field = (String) args.get(FIELD_NAME);  
        CustomField customField = customFieldManager.getCustomFieldObjectByName(field);
    
        if (customField!=null){
          //Check if the custom field value is NULL
          if (issue.getCustomFieldValue(customField) == null){
            throw new InvalidInputException("The field:"+field+" is
                 required!"); }
        }
      }
    

    执行验证的议题可以从transientVars映射中获取。customFieldManager像往常一样在构造函数中注入。

  4. 现在只需将这些组件填充到 atlassian-plugin.xml 文件中即可。我们使用 workflow-validator 模块,代码块如下所示:

    <workflow-validator key="field-validator" name="Field Validator"  class="com.jtricks.FieldValidatorFactory">
        <description>Field Not Empty Workflow Validator</description>
    
        <validator-class>com.jtricks.FieldValidator</validator-class>
    
        <resource type="velocity" name="view" location="templates/com/jtricks/view-fieldValidator.vm"/><resource type="velocity" name="input-parameters" location="templates/com/jtricks/edit-fieldValidator.vm"/>
        <resource type="velocity" name="edit-parameters" location="templates/com/jtricks/edit-fieldValidator.vm"/>
    </workflow-validator>
    
  5. 打包插件并部署它!

请注意,我们在工作流中存储的是角色名称,而不是角色 ID,这与我们在工作流条件中所做的不同。然而,使用 ID 是安全的,因为管理员可以重命名角色,这样就需要在工作流中进行相应的更改。

它是如何工作的...

插件部署后,我们需要修改工作流以包含验证器。以下截图展示了验证器初次添加时的样子。正如你现在所知道的,这通过输入模板渲染:

它是如何工作的...

在添加验证器后(选择了测试编号字段后),它将通过视图模板进行渲染,显示如下:

它是如何工作的...

如果你尝试编辑它,屏幕将使用编辑模板进行渲染,正如以下截图所示:

它是如何工作的...

请注意,测试编号字段已被选中。

配置完工作流后,当用户进入议题并尝试推进时,验证器将检查测试编号字段是否有值。正是在这个时候,FieldValidator 类中的 validate 方法被执行。

如果值缺失,你将看到一个错误,正如以下截图所示:

它是如何工作的...

另见

  • 在第一章中创建骨架插件

  • 在第一章中部署你的插件

编写工作流后置功能

现在让我们来看一下工作流后置功能。工作流后置功能非常有效且广泛使用。它们允许你在处理议题的工作流时执行很多操作。许多自定义和解决方法都是通过这条路径实现的!

工作流后置功能是通过 workflow-function 模块创建的。以下是支持的关键属性和元素。

属性

名称 描述
key 这在插件中应该是唯一的。
Class 提供渲染 velocity 模板上下文的类。如果功能不需要输入,则必须实现 com.atlassian.jira.plugin.workflow.WorkflowNoInputPluginFactory 接口;如果需要输入,则必须实现 com.atlassian.jira.plugin.workflow.WorkflowPluginFunctionFactory 接口。
i18n-name-key 插件模块的本地化名称键。
name 工作流功能的可读名称。

元素

名称 描述
description 工作流功能的描述。
function-class 执行验证的类。必须实现com.opensymphony.workflow.FunctionProvider。推荐扩展com.atlassian.jira.workflow.function.issue.AbstractJiraFunctionProvider,因为它已经实现了许多有用的方法。
resource type="velocity" 工作流功能视图的 Velocity 模板。

还有三个其他元素可以与后置功能一起使用。它们的说明如下:

  • orderable – (true/false)指定该功能是否可以在与转换关联的功能列表中重新排序。列表中的位置决定了功能的执行顺序。

  • unique – (true/false)指定该功能是否唯一,即是否可以在单个转换中添加多个该后置功能实例。

  • deletable – (true/false)指定该功能是否可以从转换中删除。

    注意

    查看confluence.atlassian.com/display/JIRADEV/Workflow+Plugin+Modules#WorkflowPluginModules-Functions以获取更多详细信息。

准备工作

和往常一样,创建一个骨架插件。使用骨架插件创建一个 Eclipse 项目,我们就可以开始了!

如何操作...

假设我们有一个用户自定义字段,并且我们希望在特定的转换发生时,将当前用户或指定的用户名设置到该自定义字段中。一个典型的应用场景是存储最后解决问题的用户的姓名。以下是编写一个通用后置功能的步骤,该功能会将当前用户名或用户提供的用户名设置到用户自定义字段中:

  1. 定义配置工作流后置功能所需的输入:

    与工作流条件和验证器不同,工作流后置功能工厂类有两个可用的接口。如果该功能不需要任何输入进行配置,则工厂类必须实现WorkflowNoInputPluginFactory。一个例子是将当前用户的姓名设置为自定义字段的值,而不是用户配置的姓名。如果需要输入来配置后置功能,则工厂类必须实现WorkflowPluginFunctionFactory。在我们的示例中,我们将用户名作为输入。

    这两个接口主要用于为模板提供 Velocity 参数。它们将用于提取定义功能时使用的输入参数。为了明确,输入这里指的不是执行工作流操作时的输入,而是定义后置功能时的输入。

    函数工厂类,SetUserCFFunctionFactory在此情况下,扩展了AbstractWorkflowPluginFactory并实现了WorkflowPluginFunctionFactory接口。与条件一样,我们需要实现三个抽象方法,分别是getVelocityParamsForInputgetVelocityParamsForEditgetVelocityParamsForView。如同名字所示,它们用于为不同场景填充速度参数:

    @Override
    protected void getVelocityParamsForEdit(Map velocityParams, AbstractDescriptor descriptor) {velocityParams.put(USER_NAME, getUserName(descriptor));
    }
    
    @Override
    protected void getVelocityParamsForInput(Map velocityParams) {
        velocityParams.put(USER_NAME, CURRENT_USER); }
    
    @Override
    protected void getVelocityParamsForView(Map velocityParams, AbstractDescriptor descriptor) {
        velocityParams.put(USER_NAME, getUserName(descriptor));
    }
    

    你可能已经注意到,这些方法看起来与工作流条件或验证器中的方法非常相似,除了业务逻辑之外!让我们详细看看这些方法:

    • getVelocityParamsForInput:此方法定义了输入场景下的速度参数,即当用户首次配置工作流时。在我们的示例中,我们需要使用一个文本字段来捕获需要添加到问题中的用户名。

    • getVelocityParamsForView:此方法定义了视图场景下的速度参数,即在配置后用户如何查看后置函数。在我们的示例中,我们定义了一个字段,因此在从工作流描述符中检索到该字段后,我们应将其显示给用户。你可能已经注意到,描述符是AbstractDescriptor的一个实例,它作为方法中的一个参数提供。我们只需要从描述符中提取用户名,代码如下所示:

      private String getUserName(AbstractDescriptor descriptor){
          if (!(descriptor instanceof FunctionDescriptor)) {
            throw new IllegalArgumentException("Descriptor must be a FunctionDescriptor.");
          }
      
          FunctionDescriptor functionDescriptor = (FunctionDescriptor) descriptor;
      
          String user = (String) functionDescriptor.getArgs().get(USER_NAME);
          if (user!=null && user.trim().length()>0)return user;
          else 
            return CURRENT_USER;
      } 
      

      只需要检查描述符是否为验证器描述符,然后按照前面的代码提取字段。

    • getVelocityParamsForEdit:此方法定义了编辑场景下的速度参数,即当用户修改现有的验证器时。在这里,我们需要选项和已选择的值。因此,我们将自定义字段集合和字段名称都放入速度参数中。

  2. 第二步是为每个场景(输入、视图和编辑)定义速度模板。我们可以在输入和编辑中使用相同的模板,只需简单检查以确保在编辑场景中保留已选的字段。让我们来看看这些模板:

    • edit-userCFFunction.vm:显示所有自定义字段,并在编辑模式下高亮显示已选中的字段:

      <tr bgcolor="#ffffff">
        <td align="right" valign="top" bgcolor="#fffff0">
          <span class="label">User Name :</span>
        </td>
        <td bgcolor="#ffffff" nowrap>
          <input type="text" name="user" value="$user"/>            <br><font size="1"> Enter the userName to be set on the Test User CustomField </font>
        </td>
      </tr>
      
    • view-userCFFunction.vm 显示已选中的字段:

      	#if ($user)
        The 'Test User' CF will be set with value : $user!
      #end
      
  3. 第三步是编写实际的函数。函数类必须扩展AbstractJiraFunctionProvider接口。我们只需要在这里实现execute方法。在我们的示例中,我们从问题中检索用户名并将其设置到Test User自定义字段上:

    public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException {
        MutableIssue issue = getIssue(transientVars);
        User user = null;
    
        if (args.get("user") != null) {
          String userName = (String) args.get("user");
          if (userName.equals("Current User")){
            // Set the current user here!
            user = authContext.getUser();
          } else {
            user = userUtil.getUser(userName);
          }
        } else {
          // Set the current user here!
          user = authContext.getUser();
        }
        // Now set the user value to the custom field
        CustomField userField = customFieldManager.getCustomFieldObjectByName("Test User");
        if (userField != null) {
          try {
            setUserValue(issue, user, userField);
          } catch (FieldLayoutStorageException e) {
            System.out.println("Error while setting the user Field");
          }
        }
     }
    

    像验证器一样,执行后置函数的议题可以通过transientVars映射来获取。用户则可以从args映射中获取。

    这里,setUserValue方法仅仅是将用户名设置到传递的自定义字段中,如下所示的代码块:

    private void setUserValue(MutableIssue issue, User user, CustomField userField) throws FieldLayoutStorageException {
        issue.setCustomFieldValue(userField, user);
        Map modifiedFields = issue.getModifiedFields();
        FieldLayoutItem fieldLayoutItem = ComponentManager.getInstance().getFieldLayoutManager().getFieldLayout(issue).getFieldLayoutItem(userField);
        DefaultIssueChangeHolder issueChangeHolder = new DefaultIssueChangeHolder();
        final ModifiedValue modifiedValue = (ModifiedValue) modifiedFields.get(userField.getId());    userField.updateValue(fieldLayoutItem, issue, modifiedValue, issueChangeHolder);
    }
    
  4. 现在剩下的就是将这些组件填充到atlassian-plugin.xml文件中。我们使用workflow-condition模块,代码如下所示:

    <workflow-function key="set-usercf" name="Set User CF Post Function" class="com.jtricks.SetUserCFFunctionFactory">
        <description>Set Defined User or Current User</description>
        <function-class>com.jtricks.SetUserCFFunction</function-class>
        <orderable>true</orderable>
        <unique>false</unique>
        <deletable>true</deletable>
    
        <resource type="velocity" name="view" location="templates/com/jtricks/view-userCFFunction.vm"/>
        <resource type="velocity" name="input-parameters"  location="templates/com/jtricks/edit-userCFFunction.vm"/>
        <resource type="velocity" name="edit-parameters" location="templates/com/jtricks/edit-userCFFunction.vm"/>
    
    </workflow-function>
    
  5. 打包插件并部署!

它是如何工作的...

插件部署后,我们需要修改工作流以包含该功能。以下是该功能与内置功能一同出现的地方:

它是如何工作的...

点击我们的后置功能将带我们进入配置页面,如下所示。正如你现在知道的,这个页面是使用输入模板渲染的:

它是如何工作的...

添加功能后(在UserName字段中输入后),显示如下:

它是如何工作的...

如果你尝试编辑,屏幕将使用编辑模板渲染,如以下截图所示:

它是如何工作的...

请注意,UserName字段已经填充。

在工作流配置完成后,当用户执行工作流操作时,Test User自定义字段将设置为值jobinkk

另见

  • 在第一章中创建一个骨架插件

  • 在第一章中部署你的插件

编辑一个活动工作流

我们已经了解了工作流在配置 JIRA 中的重要作用,以及如何编写插件来添加更多的工作流条件、验证器和后置功能。一旦这些插件被添加,我们需要修改工作流,以在适当的转换点包含新创建的组件。

修改一个非活动工作流或创建一个新工作流非常容易。你可以在创建转换时添加条件/验证器/后置功能,或者只需点击转换来修改它们。但是,编辑一个活动工作流则涉及更多的步骤,我们将在本食谱中看到这些步骤。

工作流在被用于与项目相关联的活动工作流方案时处于活动状态。你可以通过导航到管理 | 全局 设置 | 工作流来检查工作流是否处于活动状态。

如何操作...

以下是编辑活动工作流的步骤:

  1. 以 JIRA 管理员身份登录。

  2. 导航到管理 | 全局 设置 | 工作流

  3. 点击你想要编辑的工作流上的创建草稿工作流链接。该链接可以在操作栏下找到。

  4. 点击你想要修改的步骤转换

  5. 进行更改。更改将在工作流发布之前不会生效。

  6. 在完成所有更改后,如果你仍在查看修改后的工作流,请点击页面顶部的发布此草稿链接。你也可以在查看所有工作流时,在操作栏目下点击发布

  7. 如果你需要备份,请在提示时复制旧的工作流,然后点击发布

它是如何工作的...

在草稿上进行更改并点击发布后,新工作流将生效。然而,这个过程有一些限制,具体如下:

  • 你不能删除现有的工作流步骤。

  • 你不能编辑与现有步骤关联的状态。

  • 如果现有步骤没有出站转换,你不能添加任何新的出站转换。

  • 你不能更改任何现有步骤的步骤 ID。

如果你想克服这些限制,你需要复制工作流,修改副本,并通过将项目迁移到新工作流来使其生效。

在新工作流生效后,任何问题的转换都会基于新的工作流进行。

还有更多内容...

如果你想修改一个活动的工作流,从而克服上述的一些限制,但又不想经历迁移所有相关项目的痛苦,你可以考虑直接在 JIRA 数据库中进行修改。

注意,在进行此操作时,我们应小心工作流的更改。例如,如果在修改后的工作流中移除了某个状态,而某些问题仍处于该状态,则这些问题将被卡在该移除的状态下。同样的情况也会发生在移除的步骤上。

修改 JIRA 数据库中的工作流

以下是修改数据库中工作流的步骤:

  1. 将需要修改的工作流导出为 XML 文件。你可以通过工作流的操作列下的 XML 链接来完成此操作。

  2. 修改 XML 文件以包含你的更改(或者,选择在 JIRA 工作流的副本中进行更改,并将其导出为 XML)。

  3. 停止 JIRA 实例。

  4. 连接到你的 JIRA 数据库。

  5. 备份现有的数据库。如果出现问题,我们可以恢复到这个备份。

  6. 更新 JIRAWORKFLOWS 表,将 descriptor 列修改为适当工作流的新 XML 文件。当工作流的 XML 非常庞大时,可能需要依赖于数据库特定的方法来更新表。例如,如果 JIRA 连接到 Oracle 数据库,我们可以使用 Oracle XML 数据库工具 (download.oracle.com/docs/cd/B12037_01/appdev.101/b10790/xdb01int.htm)。

  7. 提交更改并断开与数据库的连接。

  8. 启动 JIRA 实例。

  9. 重新索引 JIRA。

基于工作流状态使问题可编辑/不可编辑

我们知道,问题的编辑权限是通过编辑问题 权限来控制的。这些权限是通过与项目关联的权限方案进行设置的,能够阻止或允许编辑问题,无论问题处于什么状态!但很多时候,我们需要在特定状态下阻止编辑问题。例如,防止编辑已关闭的问题。

我们将简要介绍如何使用工作流属性来实现这一点。

如何操作...

我们可以使用 jira.issue.editable 工作流属性来使问题可编辑或不可编辑。以下是逐步的操作流程:

  1. 以 JIRA 管理员身份登录。

  2. 导航到 Administration | Global Settings | Workflows

  3. 如果工作流是活动的,请创建工作流的草稿。导航到需要修改的 步骤

  4. 点击 查看步骤属性 链接。

  5. Property Key 字段中输入 jira.issue.editable

  6. 如果希望在此转换操作执行后禁止编辑该问题,请在 Property Value 字段中输入 false。如果希望使其可编辑,请输入 true 作为值。

  7. 如果工作流是活动的,请返回并发布工作流。如果不是,请将工作流与相应的方案关联。

请注意,该属性是添加在工作流的 步骤 上,而不是 转换 上。

它是如何工作的...

当查看问题时,只有在你拥有编辑权限且工作流经理将问题标记为可编辑时,编辑操作才可用。工作流经理会获取当前状态(即链接到该状态的步骤)上添加的属性列表,并检查 jira.issue.editable 属性的值,看它是否设置为 false,然后再将问题标记为可编辑。

关联到问题工作流步骤的属性可以通过以下方式获取:

JiraWorkflow workflow = workflowManager.getWorkflow(issue);
StepDescriptor currentStep = workflow.getLinkedStep(ManagerFactory.getConstantsManager().getStatus(status));
Map properties = currentStep.getMetaAttributes();

jira.issue.editable 属性值是通过 properties.get(JiraWorkflow.JIRA_META_ATTRIBUTE_EDIT_ALLOWED) 获取的,其中 JiraWorkflow.JIRA_META_ATTRIBUTE_EDIT_ALLOWED = "jira.issue.editable"

同样的方法可以用于获取在工作流步骤中添加的任何其他属性。

另见

  • 基于工作流状态的权限

为特定转换包含/排除解决方案

如果你还没注意到,JIRA 中的解决方案是全局性的。如果你有一个 Resolved 的解决方案,它将在转换屏幕中添加解决方案字段时出现。在某些情况下,这可能没有意义。例如,在拒绝问题时,添加 Resolved 解决方案是没有意义的。

让我们看看如何基于工作流转换选择解决方案。

如何操作...

我们可以通过使用 jira.field.resolution.includejira.field.resolution.exclude 属性在工作流转换中包含/排除特定的解决方案。以下是逐步操作流程:

  1. 以 JIRA 管理员身份登录。

  2. 导航到 Administration | Global Settings | Workflows

  3. 如果工作流是活动的,请创建工作流的草稿。导航到需要修改的转换。

  4. 点击 查看此转换的属性 链接。

  5. 根据是否想要包含或排除特定的解决方案,在 Property Key 字段中输入 jira.field.resolution.includejira.field.resolution.exclude

  6. Property Value 字段下输入要包含/排除的分隔符为逗号的解决方案 ID 列表。解决方案 ID 可以通过导航到 Administration | Issue Settings | Resolutions,并悬停在 Edit 链接上获取:如何操作...

    你也可以通过查询数据库中的 resolutions 表来查找分辨率 ID。

  7. 点击添加

  8. 如果工作流已激活,返回并发布工作流。如果没有,关联工作流与相应的方案。

请注意,该属性是在工作流的过渡阶段添加的,而不是在步骤中添加的。

如何工作...

jira.field.resolution.exclude 属性被添加时,在 属性值字段下以逗号分隔的分辨率 ID 将在该过渡期间从屏幕上排除。

另一方面,如果添加了 jira.field.resolution.include,则仅在 属性值字段下输入的分辨率 ID 会在屏幕上显示。

基于工作流状态的权限

我们已经看到如何根据工作流状态限制对问题的编辑。JIRA 还提供了更多选项,可以限制许多操作(如编辑、评论等)对问题或其子任务的权限,具体取决于问题的状态。

让我们详细了解一下。

如何操作...

这与使问题可编辑/不可编辑的方式类似。在这里,我们也在相关的工作流步骤上添加了一个属性。以下是步骤:

  1. 以 JIRA 管理员身份登录。

  2. 转到管理 | 全局 设置 | 工作流

  3. 如果工作流已激活,创建工作流草稿。转到需要修改的步骤。

  4. 点击查看步骤属性链接。

  5. 将权限属性输入到属性键字段中。该属性的形式为 – jira.permission.[subtasks.]{permission}.{type}[.suffix],其中:

    • subtasks – 这是可选项。如果包含该项,则权限应用于问题的子任务。如果没有,则权限应用于实际的问题。

    • permission – 在 Permissions (docs.atlassian.com/software/jira/docs/api/latest/com/atlassian/jira/security/Permissions.html) 类中指定的简短名称。

      以下是 JIRA 4.2 中的允许值:adminusesysadminprojectbrowsecreateeditscheduleissueassignassignableattachresolveclosecommentdeleteworkworklogdeleteallworklogdeleteownworklogeditallworklogeditownlinksharefiltersgroupsubscriptionsmovesetsecuritypickusersviewversioncontrolmodifyreporterviewvotersandwatchersmanagewatcherlistbulkchangecommenteditallcommenteditowncommentdeleteallcommentdeleteownattachdeleteallattachdeleteown

    • type – 授予/拒绝的权限类型。可选值有 groupuserassigneereporterleaduserCFprojectrole

    • suffix – 可选后缀,用于在添加相同类型多次时使属性唯一!例如 jira.permission.edit.group.1jira.permission.edit.group.2 等。因为 OSWorkflow 限制了属性值必须唯一。

  6. 属性值 字段中输入适当的值。如果类型是组,输入一个组。如果是用户,输入用户名,依此类推。

    在这里提供一些例子可能会很有用:

    • jira.permission.comment.group=some-group

    • jira.permission.comment=denied

    • jira.permission.edit.group.1=some-group-one

    • jira.permission.edit.group.2=some-group-two

    • jira.permission.modifyreporter.user=username

    • jira.permission.delete.projectrole=10000

    • jira.permission.subtasks.delete.projectrole=10000

    当类型未使用时,你甚至可以将值设置为 'denied'。例如,jira.permission.comment=denied 意味着在此状态下禁用评论功能。

  7. 如果工作流是激活的,返回并发布该工作流。如果没有,关联该工作流与适当的方案。

它是如何工作的...

当特定的权限属性与工作流状态绑定时,JIRA 会查看并强制执行该权限。需要注意的是,工作流权限只能限制权限方案中设置的权限,而不能授予权限。

例如,如果你在权限方案中将编辑权限限制为 jira-administrators,添加 jira.permission.edit.group=jira-users 并不会授予 jira-users 编辑权限。

但是,如果你有两个拥有编辑权限的组,那么只有 jira-users 会被允许编辑,这由工作流权限定义。

另见

  • 根据工作流状态使问题可编辑/不可编辑

工作流转换中的国际化

如果你的 JIRA 实例被全球各地讲不同语言的人使用,你可能会使用国际化将 JIRA 转换为他们自己的语言。但像工作流操作名称、按钮名称等配置是在工作流中,而不是作为 i18n 属性。因此,它们是限定于单一语言的。

这就是工作流属性再次为我们提供帮助的地方!

如何操作...

我们可以使用属性 jira.i18n.submitjira.i18n.title 分别修改工作流操作提交按钮的名称或操作名称。以下是步骤:

  1. 打开 jar 文件 atlassian-jira/WEB-INF/lib/language_<语言代码>_<国家代码>.jar。从 JIRA 4.3 开始,jar 文件名的格式为 jira-lang-<语言代码>_<国家代码>-<jira 版本>.jar

  2. 编辑 jar 文件中的 \com\atlassian\jira\web\action\JiraWebActionSupport_<语言代码>_<国家代码>.properties 文件。你可以使用 7zip 等工具编辑 jar 文件中的文件。或者,你也可以解压 jar 文件,修改文件后再重新归档!

  3. 添加你的 i18n 属性及其值:my.submit.button=我的提交按钮(英语)

  4. 更新文件并重启 JIRA 以使新属性生效。

  5. 以 JIRA 管理员身份登录。

  6. 转到 管理 | 全局 设置 | 工作流

  7. 如果工作流处于活动状态,请创建工作流草稿。导航到需要修改的转换。

  8. 点击 查看此转换的属性 链接。

  9. 根据你希望修改提交按钮名称或操作名称,分别在 属性键 字段中输入 jira.i18n.submitjira.i18n.title。让我们以提交按钮为例。

  10. 属性值 字段中输入我们在属性文件中使用的 i18n 键。在我们的示例中,键是 my.submit.button

  11. 点击 添加

  12. 如果工作流处于活动状态,请返回并发布工作流。如果不是,请将工作流与适当的方案关联。

它是如何工作的...

一旦工作流发布,JIRA 将从 i18n 属性文件中填充提交按钮名称,当下次发生转换时。在我们的示例中,转换屏幕将显示如下截图:

它是如何工作的...

正如你所看到的,按钮名称已经更改为 My Submit Button in English。现在你需要做的就是修改其他语言的 jar 文件,以包含正确的翻译!

另见

  • v2 插件中的国际化

编程方式获取可用的工作流操作

在我们的程序中,我们经常遇到需要检索当前工作流操作的情况,特别是可用的操作。让我们看看如何使用 JIRA API 来实现这一点。

如何操作...

按照以下步骤操作:

  1. 检索与问题关联的 JIRA 工作流对象:

    JiraWorkflow workFlow = componentManager.getWorkflowManager().getWorkflow(issue);
    

    这里,issue 是当前的问题,它是 com.atlassian.jira.issue.Issue 类的一个实例。

  2. 获取问题状态,并使用它检索当前与问题关联的工作流步骤:

    GenericValue status = issue.getStatusObject().getGenericValue();
    com.opensymphony.workflow.loader.StepDescriptor currentStep = workFlow.getLinkedStep(status);
    
  3. 从当前步骤中检索可用操作集:

    List<ActionDescriptor> actions = currentStep.getActions();
    

    这里,actions 是一个 com.opensymphony.workflow.loader.ActionDescriptor 列表。

  4. 遍历 ActionDescriptors 并根据需求获取每个操作的详细信息!可用操作的名称可以按如下方式打印:

    for (ActionDescriptor action : actions) {
        System.out.println("Action: "+action.getName())
     }
    

它是如何工作的...

WorkflowManager 用于执行与工作流相关的许多操作,如创建/更新/删除工作流、复制工作流、创建草稿等。在这里,我们使用它根据选定的问题来检索工作流对象。请查看 API(docs.atlassian.com/jira/latest/com/atlassian/jira/workflow/WorkflowManager.html)以获取 WorkflowManager 可用操作的完整列表。

一旦我们检索到 JIRA 工作流,我们通过状态获取当前步骤。正如你在本章之前所看到的,工作流 状态 与一个且仅一个工作流 步骤 相关联。一旦我们获取到 步骤,就可以从中获取大量信息,包括该 步骤 可用的操作。

好极了?

还有更多内容...

还有更多内容...

获取给定名称的操作 ID

同样的方法也可以用来根据操作名称检索操作 ID。记住,我们在程序化推进工作流时使用的是操作 ID。

一旦操作名称可用,你可以通过迭代操作列表轻松获取操作 ID,如以下代码行所示:

private int getActionIdForTransition(List<ActionDescriptor> actions, String actionName) {
  for (ActionDescriptor action : actions) {
    if (action.getName().equals(actionName)) {
               return action.getId();
          }
  }
  return -1; // Handle invalid action
}

在工作流上进行程序化进度

我们通常对工作流执行的另一个操作是通过编程方式将问题通过工作流过渡。让我们来看一下如何使用 JIRA API 执行此操作。

如何做到……

从 JIRA 4.1 起,转换问题是通过IssueService完成的(docs.atlassian.com/jira/latest/com/atlassian/jira/bc/issue/IssueService.html)。下面是如何做到的:

  1. 通过构造函数注入或如下所示获取IssueService对象:

    IssueService issueService = ComponentManager.getInstance().getIssueService();
    
  2. 查找要执行的操作的操作 ID。你可以通过查看工作流(括号内的数字以及转换名称)来获取,如果你确定它不会改变,或者使用操作名称来检索它(参考之前的步骤)。

  3. 如果你想修改问题的任何内容,例如指派人、报告人、解决方案等,请填充IssueInputParameters!它代表一个问题构建器,用于提供可在转换过程中更新问题的参数:

    IssueInputParameters issueInputParameters = new IssueInputParametersImpl();
    issueInputParameters.setAssigneeId("someotherguy");
    issueInputParameters.setResolutionId("10000");
    
  4. 完整的支持字段列表可以在docs.atlassian.com/jira/latest/com/atlassian/jira/issue/IssueInputParameters.html找到。

  5. 验证转换:

    TransitionValidationResult transitionValidationResult = issueService.validateTransition(user, 12345L, 10000L, issueInputParameters);
    
    • User – 当前用户或将执行转换的用户

    • 12345L – 问题 ID

    • 10000L – 操作 ID

    • issueInputParameters – 我们在之前步骤中填充的参数

  6. 如果transitionValidationResult有效,调用转换操作。如果无效,请处理它。确保使用相同的用户。

    if (transitionValidationResult.isValid()){
            IssueResult transitionResult = issueService.transition(user, transitionValidationResult);
            if (!transitionResult.isValid()){
                // Do something
            }
    }
    

    我们还需要对结果进行最后的检查,以查看它是否有效!

  7. 这将使问题转换到适当的状态。

IssueService之前,转换是通过WorkflowTransitionUtil完成的(docs.atlassian.com/jira/latest/com/atlassian/jira/workflow/WorkflowTransitionUtil.html)。它仍然被支持,但推荐使用IssueService

以下是如何使用WorkflowTransitionUtil进行转换的示例:

获取WorkflowTransitionUtil对象:

WorkflowTransitionUtil workflowTransitionUtil = (WorkflowTransitionUtil) JiraUtils .loadComponent(WorkflowTransitionUtilImpl.class);

创建一个需要更新的参数映射:

Map paramMap = EasyMap.build(); 
paramMap.put(IssueFieldConstants.RESOLUTION, "10000");  paramMap.put(IssueFieldConstants.COMMENT, comment);

使用以下详细信息填充workflowTransitionUtil

workflowTransitionUtil.setParams(paramMap);  workflowTransitionUtil.setIssue(12345L);  workflowTransitionUtil.setUsername(user);  workflowTransitionUtil.setAction(10000L);

验证转换:

ErrorCollection c1 = workflowTransitionUtil.validate();

如果没有错误,继续工作流。处理错误(如果有的话):

ErrorCollection c2 = workflowTransitionUtil.progress();

然后我们应该看到问题处于新的状态!检查错误集合以处理错误(如果有的话)。

它是如何工作的……

一旦操作 ID 正确且参数经过正确验证,IssueServiceWorkflowTransitionUtil将进行后台工作,完成问题的状态转换。

从数据库获取工作流历史记录

JIRA 会在其“更改历史”中捕捉问题的更改。通过访问问题查看页面的“更改历史”标签,你可以轻松找到它们。

但是,我们通常希望了解问题在生命周期中经过的各种工作流状态的具体细节。当一个问题有成百上千次更改时,通过更改历史记录找出状态变化是一个痛苦的任务。人们通常编写插件来绕过这个问题,或者直接访问数据库。

即使是通过插件实现的,后台逻辑也是查看数据库中的表。在本教程中,我们将查看相关表,并编写 SQL 查询以提取给定问题的工作流更改。

准备工作

确保你已安装并配置了一个 SQL 客户端,它将帮助你连接到 JIRA 数据库。

如何操作...

按照以下步骤操作:

  1. 连接到 JIRA 数据库。

  2. 找出你想提取工作流更改的id。如果你手头没有 ID,可以通过如下方式使用问题关键字从数据库中获取它:

    select id from jiraissue where pkey = "JIRA-123"
    
    

    其中,JIRA-123是问题的关键字。

  3. 提取为问题创建的所有更改组。每次对问题进行的更改(例如,编辑、工作流转换等)都会被 JIRA 分组到一个单独的changegroup中。JIRA 在changegroup记录中存储相关的issueidcreated日期(即更改发生的日期):

    select id from changegroup where issueid = '10010'
    
    

    其中,10010issueid,即我们在前一步中提取的 ID。

    在提取更改组时,我们甚至可以提到创建日期,如果你只想查看特定日期的更改,可以使用author字段限制只查看某个用户做出的更改。

  4. 提取所选组的status更改:

    select oldstring, newstring from changeitem where fieldtype = "jira" and field = "status" and groupid in ( 10000, 10010 )
    
    

    这里的groupid 1000010010等是前一步中提取的 ID。这里,oldstring是问题上的原始值,newstring是更新后的值。

    如果你想获取状态 ID,也可以包括oldvaluenewvalue

你可以像下面那样编写一个单独的查询,或者修改它以包含更多的细节。但希望这能为你提供一个起点!

select oldstring, newstring from changeitem where fieldtype = "jira" and field = "status" and groupid in ( select id from changegroup where issueid = '10010');

另一个提取细节以及创建日期的示例是使用内连接,如下所示:

select ci.oldstring, ci.newstring, cg.created from changeitem ci inner join changegroup cg on ci.groupid = cg.id where ci.fieldtype = "jira" and ci.field = "status" and cg.issueid = '10010';

现在交给你们,DBA 们!

它是如何工作的...

如前所述,对问题进行的每次操作的更改都会作为changegroup记录存储在 JIRA 数据库中。主要的三个列issueidauthorcreated都是这个表的一部分。

实际的更改存储在changeitem表中,其外键groupid指向changegroup记录。

在我们的例子中,我们专门查看工作流状态,因此我们查询具有 fieldtype 值为 jirafieldstatus 的记录。

查询结果(使用内连接)如下:

如何操作...

另见

  • 从表格中获取工作流详细信息

在 JIRA 中重新排序工作流操作

在 JIRA 工作流中,显示在 查看问题 页面上的可用操作通常按这些过渡创建的顺序排列。大多数时候这种方式是有效的,但在某些情况下,我们可能需要改变它在问题页面上显示的顺序!

为了在 查看问题 页面上实现工作流操作的逻辑排序,JIRA 为我们提供了一个名为 opsbar-sequence 的工作流属性。让我们看看如何使用此属性来修改顺序,而不是直接修改工作流。

如何操作...

请按照以下步骤操作:

  1. 以 JIRA 管理员身份登录

  2. 导航到 管理 | 全局 设置 | 工作流

  3. 如果工作流是活动状态,则创建工作流的草稿。导航到需要修改的过渡。

  4. 点击 查看此过渡的属性 链接。

  5. 属性键 字段中输入 opsbar-sequence

  6. 属性值 字段下输入 序列 值。此值应相对于其他过渡中输入的值。

  7. 点击 添加

  8. 返回并发布工作流,如果它处于活动状态。如果没有,将工作流与适当的方案关联。

请注意,属性是添加在工作流 过渡 上,而不是 步骤 上。

如何操作...

让我们考虑以下示例,其中 拒绝此操作 工作流操作出现在最前面:

如何操作...

通常,人们会希望将此作为最后一个选项,因为它很可能是最少使用的操作。

由于这里有四个操作,我们可以按照下表所示的顺序进行排列,并对其进行序列值标注:

工作流操作 序列
开始进度 10
解决问题 20
关闭问题 30
拒绝此操作 40

请注意,序列号甚至可以是 1、2、3 和 4。没有限制数字从何处开始或如何排列。建议使用 10、20 等序列,以便在将来需要时可以插入新的过渡。

在我们使用属性和前述的序列号修改工作流之后,正如我们在前一节所看到的,操作顺序如下:

如何操作...

请记住,工作流操作的顺序仅在 查看问题 页面中更改,而不是在 查看工作流步骤 页面中更改,后者是你修改工作流步骤的地方。

在工作流中创建常见的过渡

配置工作流可能是件痛苦的事情,尤其是在 10 个不同位置使用了相似的过渡,而且这些过渡时常发生变化。变更可能是最简单的事情,比如仅仅修改过渡的名称,但我们最终还是需要在 10 个地方进行修改。

这就是 OSWorkflow 的常见操作来拯救我们的地方。可以在www.opensymphony.com/osworkflow/3.3%20Common%20and%20Global%20Actions.html阅读一些理论内容。

JIRA 已经在其默认工作流中使用了常见操作。我们无法修改默认工作流,但如果我们复制一份并将 Resolve 过渡重命名为 New Resolve,它将显示如下截图所示:

在工作流中创建常见过渡

请注意,过渡在出现的三个地方都被重命名了!

在本教程中,让我们来看看如何添加一个新的常见过渡。

如何操作...

添加常见过渡有两种方式。

  1. 复制 JIRA 默认工作流并修改以适应我们的需求。

  2. 使用 XML 创建工作流。

第一个方法仅在我们的工作流需求有限时才有用,也就是说,只有在我们可以通过修改现有过渡来解决问题时才适用。

如果我们需要配置一个包含新常见过渡的大型工作流,我们需要采取 XML 路径。让我们来看一下步骤:

  1. 为了简化操作,将需要修改的现有工作流导出为 XML。你可以通过查看工作流页面上的 XML 链接来完成此操作。你也可以从零开始创建工作流 XML,但这需要大量的努力和对 OSWorkflow 的了解。在本例中,我们导出了标准的 JIRA 工作流。

  2. 在工作流 XML 中找到 common-actions 部分,它位于 initial-actions 后面紧接着出现。

  3. 添加我们新的 common-action。在这里有几点需要注意。操作 ID 应该是 XML 中唯一的 ID。你可以在标准工作流 XML 中找到所有这些的示例,或者你也可以在 OSWorkflow 文档中阅读更多内容。

    以下是一个简单操作的样子:

    <action id="6" name="Start Again">
      <meta name="jira.description">Testing Common Actions</meta> 
      <results>
        <unconditional-result old-status="Finished" status="Open" step="1">
           <post-functions>
             <function type="class">
               <arg name="class.name">com.atlassian.jira.workflow.function.issue.UpdateIssueStatusFunction</arg>
             </function>
             <function type="class">
               <arg name="class.name">com.atlassian.jira.workflow.function.misc.CreateCommentFunction</arg>
             </function>
             <function type="class">
               <arg name="class.name">com.atlassian.jira.workflow.function.issue.GenerateChangeHistoryFunction</arg>
             </function>
             <function type="class">
               <arg name="class.name">com.atlassian.jira.workflow.function.issue.IssueReindexFunction</arg>
             </function>
             <function type="class">
               <arg name="class.name">com.atlassian.jira.workflow.function.event.FireIssueEventFunction</arg>
                <arg name="eventTypeId">13</arg>
              </function>
           </post-functions>
        </unconditional-result>
      </results>
    </action>
    
  4. 确保你修改名称、描述、状态、步骤、eventTypeId 和后置功能。在这里我们使用 Finished 作为 old-status,因为它在 JIRA 标准工作流的其他常见操作中也被使用。你还可以添加新的元数据属性、条件、验证器等等,但通常最好从简单开始,并在导入到 JIRA 后通过 JIRA UI 修改其他所有内容。

  5. 在其他步骤中根据需要加入常见操作:

    <step id="1" name="Open">
      <meta name="jira.status.id">1</meta>
      <actions>
        <common-action id=".." />
        .....................
        <common-action id="6" />
        <action id=" .....
        .......................
        </action>
      </actions>
    </step>
    

    请注意,这里的 ID 应该是我们在前面步骤中添加的 common-action 的操作 ID。此外,common-actions 应该出现在 step 中的 action 元素之前,以符合 OSWorkflow 语法。

  6. 将修改过的 XML 作为工作流导入到 JIRA 中。你可以通过 从 XML 导入工作流 链接来完成。有关详细信息,请查看confluence.atlassian.com/display/JIRA/Configuring+Workflow#ConfiguringWorkflow-UsingXMLtocreateaworkflow

  7. 现在,工作流已经准备好使用。

它是如何工作的...

JIRA 的工作流本质上使用 OpenSymphony 的 OSWorkflow,正如我们在第二章,理解插件框架中所见。OSWorkflow 使我们能够通过修改工作流 XML 来添加常见的操作。我们已经通过修改现有的工作流 XML 并将其导入回 JIRA 来使用此功能。

以下截图展示了更新后的工作流:

它是如何工作的...

请注意,新的过渡 重新开始 已添加到除最后一个步骤之外的所有步骤。如果我们想将其名称修改为 重新开始 & 再次开始,只需编辑其中一个过渡即可。修改后的工作流如下所示:

它是如何工作的...

我们也可以类似地修改过渡中的任何属性,它将在所有使用该过渡的地方得到反映。

Jelly 升级

在结束本章之前,让我们快速看一下如何使用 JIRA 的一个有用功能,通过将问题转移到预定义的工作流状态来提升非活动问题的优先级。

Jelly 服务是 JIRA 中的一个内置服务,使用它我们可以在定时的间隔运行有用的 Jelly 脚本。Atlassian 在其文档中解释了如何运行 Jelly 脚本,将在过去七天内没有更新的任务移至非活动状态,具体请见confluence.atlassian.com/display/JIRA/Jelly+Escalation

让我们看一下如何修改脚本,并将问题转移到不同的工作流状态的这个示例。

准备工作

确保在你的 JIRA 实例中启用了 Jelly。由于安全原因,默认情况下它是禁用的。你可以通过将 jira.jelly.on 属性设置为 true 来启用它。

你可以通过将 -Djira.jelly.on=true 添加到 JAVA_OPTS 变量中来设置此属性。添加此变量取决于服务器和操作系统。

例如,可以通过将其添加到 /bin 文件夹下的 setenv.bat 中,在 Windows 上的 Tomcat 服务器上设置该属性。

如何操作...

以下是关闭过去 15 天内没有活动的任务的步骤:

  1. 创建一个筛选器,显示过去 15 天内未更新的任务。你可以通过执行以下 JQL 查询来做到这一点:

    updated <= -15d
    

    保存筛选器并记录筛选器 ID。

    如何操作...

  2. 你可以通过将鼠标悬停在编辑链接上来找到过滤器 ID,如图所示。URL 将类似于 http://localhost:8080/secure/EditFilter!default.jspa?atl_token=084b891405e500819d6443d8378ed37a5bbe4c72&filterId=10010&returnUrl=ManageFilters.jspa,其中 filterId10010

    修改由 Atlassian 提供的 Jelly 脚本,以包含新的过滤器 ID、工作流步骤名称、用户名和密码。同时相应地修改注释。

    这是修改后的脚本:

    <JiraJelly    >
    <jira:Login username="jobinkk" password="[password here]">    <log:warn>Running Inactivate issues service</log:warn>
        <!-- Properties for the script -->
        <core:set var="comment">Closing out this issue since it has been inactive for 15 days!</core:set>
        <core:set var="workflowStep" value="Close Issue" />
        <core:set var="workflowUser" value="jobinkk" />
        <core:set var="filter15Days" value="10010" />
    
        <!-- Run the SearchRequestFilter -->
        <jira:RunSearchRequest filterid="${filter15Days}" var="issues" />
    
        <core:forEach var="issue" items="${issues}">
        <log:warn>Inactivating issue ${issue.key}</log:warn>
    
         <jira:TransitionWorkflow key="${issue.key}" user="${workflowUser}" workflowAction="${workflowStep}" comment="${comment}"/>   </core:forEach>
    </jira:Login>
    </JiraJelly>
    
  3. 保存脚本并将其放置在 JIRA 运行的服务器上的某个位置。

  4. 转到管理 | 系统 | 服务 JIRA 中。

  5. 添加升级服务:

    • 名称:升级任务

    • :点击内置服务并选择运行 Jelly 脚本。类将被选为 com.atlassian.jira.jelly.service.JellyService

    • 延迟:选择一个合适的延迟时间(以分钟为单位)。

    • 点击添加服务

  6. 添加服务页面,输入以下详细信息:

    • 输入文件:我们在服务器中保存的脚本文件路径

    • 输出文件:输出日志文件的路径。

    • 延迟:如有需要,进行修改。

脚本现在将运行配置的延迟。

它是如何工作的...

JIRA 有自己专用的Jelly 脚本 API。从脚本中可以看到,以下是执行的步骤:

  1. 脚本使用 RunSearchRequest 方法在我们在第一步保存的过滤器上运行搜索请求。然后它将检索到的结果存储在变量 issues 中。

  2. 脚本会遍历所有问题,并使用 TransitionWorkflow 方法在工作流中对每个问题进行过渡。它使用问题的键、我们配置的工作流用户和工作流操作。同时它还会添加我们在脚本中输入的评论。

    请注意,工作流操作应当在问题的当前状态下可用。如果不可用,过渡将无法执行。例如,如果在已经关闭的任务上尝试使用关闭工作流操作,将会抛出错误。

我们可以修改脚本,以根据任何过滤器条件将问题过渡到任何工作流状态。

关于 Jelly 脚本的更多有用信息可以在 confluence.atlassian.com/display/JIRA/Jelly+Tags 中找到。

第五章:JIRA 中的小工具和报告

在本章中,我们将涵盖:

  • 编写 JIRA 报告

  • Excel 格式的报告

  • JIRA 报告中的数据验证

  • 限制报告访问权限

  • 报告的对象配置参数

  • 在 JIRA 中创建饼图

  • 编写 JIRA 4 小工具

  • 从小工具中调用 REST 服务

  • 配置小工具中的用户偏好设置

  • 访问 JIRA 外部的小工具

介绍

在像 JIRA 这样的应用程序中,报告支持是不可避免的!由于数据跨越不同的项目、问题,并且大量的项目规划都在其上进行,我们需要越来越多的根据需求定制的数据报告。

JIRA 提供两种不同类型的报告:

  1. 可以添加到用户仪表板的小工具——从 4.x 版本开始,JIRA 仪表板被重新设计为包含小工具,取代了旧版的 portlet。这些小工具是使用 HTML 和 JavaScript 构建的迷你应用程序,可以在任何 OpenSocial 小工具容器中运行。它们通过 REST API 与 JIRA 进行通信,获取所需的信息,然后为用户适当渲染显示。

    由于 JIRA 仪表板现在是一个 OpenSocial 小工具容器,我们甚至可以将第三方小工具添加到其中,前提是它们符合小工具规范。同样,JIRA 小工具也可以添加到其他容器中,如 iGoogle、Gmail 等,但并不是所有 JIRA 小工具的功能都被其他小工具容器所支持。

  2. 普通 JIRA 报告——JIRA 还提供创建报告的选项,这些报告显示特定人员、项目、版本或问题中的其他字段的统计信息。这些报告可以在“浏览项目”下找到,可以用来生成简单的表格报告、图表等,如果支持,还可以导出到 Excel。

    JIRA 提供了许多内置报告,详细信息可以在 confluence.atlassian.com/display/JIRA/Generating+Reports 中找到。

除了 JIRA 提供的小工具和报告外,Atlassian 插件交换平台上还有很多其他可用的小工具和报告。但最终,我们还会编写一些专门为我们组织定制的报告,而这正是 JIRA 的插件架构为我们提供帮助的地方,它提供了两个插件模块,一个用于报告,一个用于小工具。

在本章中,我们将详细介绍编写 JIRA 报告和小工具、将旧版 portlet 转换为小工具等内容。

除此之外,我们还将简要了解 JIRA 查询语言 (JQL),它提供了在问题导航器中进行高级搜索的能力。JQL 帮助我们在问题导航器中生成许多报告,并将其导出为 Excel、Word 等方便的视图。

编写 JIRA 报告

正如我们刚刚提到的,JIRA 报告可以基于 JIRA 中的所有元素显示统计信息——例如,问题、项目、用户、问题类型等。它们可以包含 HTML 结果,并可选地包含 Excel 结果。

要在 JIRA 中添加新的报告,你可以使用 报告插件模块。以下是支持的关键属性和元素:

属性

名称 描述
key 这个键在插件中应该是唯一的。
class 提供渲染 Velocity 模板上下文的类。必须实现 com.atlassian.jira.plugin.report.Report 接口。推荐继承 com.atlassian.jira.plugin.report.impl.AbstractReport 类。
i18n-name-key 插件模块可读名称的本地化键。
name 报告的可读名称。出现在插件页面中。默认值为插件键。

元素

名称 描述
description 报告的描述。
label 用户可见的报告名称。
resource type="velocity" 报告视图的 Velocity 模板。
resource type="18n" 用于 i18n 本地化的 JAVA 属性文件
properties 用于接受用户输入的报告配置参数。

准备就绪

使用 Atlassian 插件 SDK 创建一个骨架插件。

如何做...

让我们考虑创建一个非常简单的报告,里面只有少量的业务逻辑。我们在这里选择的示例是显示所选 项目 中所有 问题 的键和值。报告的唯一输入将是 项目名称,可以从下拉列表中选择。

以下是创建此报告的逐步过程:

  1. 在插件描述符中添加报告插件模块。

    在第一步中,我们将着眼于在 atlassian-plugin.xml 文件中填充整个插件模块。

    1. 包含报告模块:

      <report key="allissues-report" name="All Issues Report"      class="com.jtricks.AllIssuesReport">
        <description key="report.allissues.description">This report shows details of all isses a specific project. </description>
         <!-- the label of this report, which the user will use to select it -->        <label key="report.allissues.label" />
      </report>
      

      像往常一样,插件模块应该具有唯一的键。这里的另一个最重要的属性是类。在这个例子中,AllIssuesReport 类用于填充 Velocity 模板的上下文,这些模板用于报告显示。它包含根据用户输入的标准检索报告结果的业务逻辑。

    2. 在报告中包含 i18n 属性资源,这些资源可以用于国际化。输入的键,如 report.allissues.label,将映射到属性文件中的一个键:

      <!-- this is a .properties file containing the i18n keys for this report -->
      <resource type="i18n" name="i18n" location="com.jtricks.allissues.AllIssuesReport" />
      

      在这里,AllIssuesReport.properties 文件将在插件的 resources 文件夹中的 com.jtricks.allissues 包下。你使用的所有键应该在属性文件中存在,并具有适当的值。

    3. 在报告模块中包含 Velocity 模板资源:

      <!-- the 'view' template is used to render the HTML result -->
      <resource type="velocity" name="view" location="templates/allissues/allissues-report.vm" />
      

      在这里,我们定义了用于渲染报告的 HTML 和 Excel 视图的 Velocity 模板。

    4. 定义用户驱动的属性:

      <!-- the properties of this report which the user must select before running it -->
       <properties>
        <property>
          <key>projectId</key>
          <name>Project</name>
          <description>report.allissues.project.description</description>
           <!-- valid types are string, text, long, select, date etc-->
           <type>select</type>
           <!-- the values generator is a class which will generate values for this select list -->
           <values class="com.jtricks.ProjectValuesGenerator"/>
        </property>
      </properties>
      

      这是一个将在报告输入页面上正确渲染的属性列表。在我们的示例中,我们需要在生成报告之前从选择列表中选择一个项目。为此,我们在这里定义了一个项目属性,其类型为select。JIRA 会通过从ProjectValuesGenerator类中获取键值对来自动将其渲染为选择列表。在接下来的示例中,我们将看到支持的更多类型的详细信息。

    现在,我们已经填写了报告插件模块所需的插件描述符。整个模块现在看起来如下:

    <report key="allissues-report" name="All Issues Report"  class="com.jtricks.AllIssuesReport">
       <description key="report.allissues.description">This report shows details of all isses a specific project. </description>
      <label key="report.allissues.label" />
      <resource type="velocity" name="view" location="templates/allissues/allissues-report.vm" />
      <resource type="i18n" name="i18n" location="com.jtricks.allissues.AllIssuesReport" />
      <properties>
        <property>
            <key>projectId</key>
            <name>Project</name>
            <description>report.allissues.project.description</description>
            <type>select</type>
            <values class="com.jtricks.ProjectValuesGenerator"/>
        </property>
        </properties>
    </report>
    
  2. 创建i18n资源属性文件。如前所述,它将创建在com.jtricks.allissues包下的 resources 文件夹中。文件名将为AllIssuesReport.properties。到目前为止,我们已经使用了三个属性,它们将填充适当的值:

    report.allissues.description=Displays all Issues from a project
    report.allissues.label=All Issues report
    report.allissues.project.description=Project to be used as the basis of the report
    

    你可以创建AllIssuesReport.proprties_{language}_{countrycode}来支持其他区域设置。

  3. 创建值生成器类。这是用于生成要在报告输入页面上呈现用户属性的值的类。在我们的示例中,我们使用了ProjectValuesGenerator类。

    生成值的类应该实现ValuesGenerator接口。然后它应该实现getValues()方法,返回一个键值对映射。值将用于显示,键将作为属性值返回,并在报告类中使用。

    ProjectValuesGenerator类中,我们使用项目 ID 和名称作为键值对。

    public class ProjectValuesGenerator implements ValuesGenerator{
      public Map<String, String> getValues(Map userParams) {
        Map<String, String> projectMap = new HashMap<String, String>(); 
        List<Project> allProjects = ComponentManager.getInstance().getProjectManager().getProjectObjects(); 
        for (Project project : allProjects) {        
          projectMap.put(project.getId().toString(), project.getName());
        }
        return projectMap;
      }
    }
    
  4. 创建报告类。这是实际业务逻辑所在的地方。

    报告类(在本例中为AllIssuesReport)应该继承AbstractReport类。它可以仅实现Report接口,但AbstractReport已经实现了一些方法,因此推荐使用它。

    我们需要在这里实现的唯一必需方法是generateReportHtml方法。我们需要在这里填充一个映射表,用来渲染 velocity 视图。在我们的示例中,我们用变量 issues 填充了映射表,它是选定项目中的问题对象列表。

    可以使用在atlassian-plugin.xml文件中的属性中输入的键值来检索选定的项目:

      final String projectid = (String) reqParams.get("projectId");
      final Long pid = new Long(projectid);
    

    我们现在使用这个pid来通过方法getIssuesFromProject检索问题列表:

      List<Issue> getIssuesFromProject(Long pid) throws SearchException {
        JqlQueryBuilder builder = JqlQueryBuilder.newBuilder();
        builder.where().project(pid);
        Query query = builder.buildQuery();
        SearchResults results = ComponentManager.getInstance().getSearchService().search(ComponentManager.getInstance().getJiraAuthenticationContext().getUser(), query, PagerFilter.getUnlimitedFilter());
        return results.getIssues();
      }
    

    现在,我们只需要在这里用这个值填充映射表,并返回如下所示的渲染视图:

      final Map<String, Object> velocityParams = new HashMap<String, Object>();
      velocityParams.put("issues", getIssuesFromProject(pid));
      return descriptor.getHtml("view", velocityParams);
    

    你可以像这样填充任何有用的变量,然后它可以在 velocity 模板中使用,以渲染视图。

    该类现在如下所示:

    public class AllIssuesReport extends AbstractReport {
    
      public String generateReportHtml(ProjectActionSupport action, Map reqParams) throws Exception {
        return descriptor.getHtml("view", getVelocityParams(action, reqParams));
      }
    
    private Map<String, Object> getVelocityParams(ProjectActionSupport action, Map reqParams) throws SearchException {
        final String projectid = (String) reqParams.get("projectId");
        final Long pid = new Long(projectid);
    
        final Map<String, Object> velocityParams = new HashMap<String, Object>();
        velocityParams.put("report", this);
        velocityParams.put("action", action);
        velocityParams.put("issues", getIssuesFromProject(pid));
        return velocityParams;
      }
    
      List<Issue> getIssuesFromProject(Long pid) throws SearchException {
        JqlQueryBuilder builder = JqlQueryBuilder.newBuilder();
        builder.where().project(pid);
        Query query = builder.buildQuery();
        SearchResults results =   ComponentManager.getInstance().getSearchService().search(ComponentManager.getInstance().getJiraAuthenticationContext().getUser(), query, PagerFilter.getUnlimitedFilter());
        return results.getIssues();
      }
    }
    
  5. 创建 velocity 模板。在我们的例子中,我们使用templates/allissues/allissues-report.vm。我们将使用在报告类中填充的 issues 变量,遍历它,并显示问题的键和摘要:

    <table id="allissues-report-table" border="0" cellpadding="3" cellspacing="1" width="100%">
       <tr class="rowNormal">
        <th>Key</th>
        <th>Summary</th>
      </tr>
       #foreach ($issue in $issues)
         <tr class="rowNormal">
          <td>$issue.key</td>
           <td>$issue.summary</td>
        </tr>
       #end
    </table>
    
  6. 至此,我们的报告已准备好。将插件打包并部署。我们将在接下来的例子中看到更多关于创建 Excel 报告、报告验证等内容。

如何实现...

它的工作逻辑可以概括如下:

  • 报告的输入视图是通过对象的可配置属性生成的,这是一组用于填充 JIRA 输入参数的预定义属性。在我们的示例中,我们使用了select属性。我们将在本章稍后详细介绍这一点。

  • 报告类获取属性,使用这些属性获取报告中所需的详细信息,并将详细信息填充到 velocity 上下文中。

  • Velocity 模板使用其上下文中的详细信息来渲染报告。

插件部署后,您可以在浏览项目部分看到报告,正如下面的截图所示:

它是如何工作的...

点击报告后,会显示输入界面,该界面使用在插件描述符中输入的属性构建,对于我们的案例是Project下拉框:

它是如何工作的...

点击下一步后,报告将使用 Report 类生成,并通过 velocity 模板渲染,具体如下:

它是如何工作的...

另见

  • 在第一章中创建一个骨架插件插件开发过程

  • 在第一章中部署您的插件

Excel 格式报告

在之前的例子中,我们看到了如何编写一个简单的报告。现在我们将看到如何修改报告插件,以包括 Excel 报告。

准备中

创建报告插件,正如在之前的例子中提到的。

如何实现...

以下是包括导出报告到 Excel 的步骤。

  1. 如果尚未添加,请在插件描述符中为 Excel 视图添加 velocity 资源类型:

    <resource type="velocity" name="excel" location="templates/allissues/allissues-report-excel.vm" />
    
  2. 在报告类中重写isExcelViewSupported方法,返回 true。在我们的案例中,我们在AllIssuesReport.java中添加了这一点:

    @Override
    public boolean isExcelViewSupported() {
      return true;
    }
    

    默认情况下,这个方法返回 false,因为它在AbstractReport类中是这样实现的。

  3. 重写generateReportExcel方法,返回 Excel 视图。这与我们在之前的例子中实现的generateReportHtml非常相似。唯一的区别是返回的视图不同。该方法如下所示:

    @Override
    public String generateReportExcel(ProjectActionSupport action, Map reqParams) throws Exception {
      return descriptor.getHtml("excel", getVelocityParams(action, reqParams));
    }
    

    这里的getVelocityParams方法与之前例子中的generateReportHtml方法完全相同。它检索问题列表并使用变量名 issues 填充 velocity 参数映射。

  4. 创建 Excel velocity 模板。模板的创建与其他模板一样,使用 HTML 标签和 velocity 语法。在我们的示例中,它将在resources下的templates/allissues/文件夹中,文件名为allissues-report-excel.vm。这里是可以定制 Excel 视图的地方。

    在我们的示例中,我们所拥有的仅仅是问题的摘要和关键字列表。因此,我们甚至可以使用相同的模板来生成 Excel。它如下所示:

    <table id="allissues-report-table" border="0" cellpadding="3" cellspacing="1" width="100%">
       <tr class="rowNormal">
        <th>Key</th>
        <th>Summary</th>
       </tr>
       #foreach ($issue in $issues)
         <tr class="rowNormal">
          <td>$issue.key</td>
          <td>$issue.summary</td>
         </tr>
       #end
    </table>
    
  5. 打包插件并部署它。

它是如何工作的...

一旦 Excel 视图被添加到报告中,生成的报告右上方将出现一个Excel 视图的链接,如下一个截图所示:

它是如何工作的...

点击链接时,将执行generateReportExcel方法,这将生成报告并使用插件描述符中定义的适当模板呈现 Excel 视图。

还有更多内容...

你可能已经注意到,当你点击Excel 视图链接时,打开的 Excel 报告名为ConfigureReport!excelView.jspa,我们需要将其重命名为.xls以便兼容 Excel。

为了自动执行此操作,我们需要在响应头中设置content-disposition参数,如下所示:

final StringBuilder contentDispositionValue = new StringBuilderStringBuffer(50);
    contentDispositionValue.append("attachment;filename=\"");
    contentDispositionValue.append(getDescriptor().getName()).append(".xls\";");  
final HttpServletResponse response = ActionContext.getResponse();
    response.addHeader("content-disposition", contentDispositionValue.toString());

此代码段在generateReportExcel方法中添加,在返回 Excel 视图之前使用描述符。报告将作为.xls文件打开,然后可以在 Excel 中打开,而无需重命名。

还有更多...

注意

请参阅support.microsoft.com/kb/260519jira.atlassian.com/browse/JRA-8484以获取一些详细信息。

另见

  • 编写 JIRA 报告

JIRA 报告中的数据验证

每当我们获取用户输入时,验证它们总是一个好主意,以确保输入符合预期的格式。这同样适用于报告。正如我们在之前的食谱中所见,JIRA 报告接受用户输入,并基于这些输入生成报告。在我们使用的示例中,选择了一个项目,并显示该项目中问题的详细信息。

在之前的示例中,错误选择项目的可能性较低,因为项目是从有效的可用项目列表中选择的。但仍然,生成报告的最终 URL 可以被篡改以包含错误的项目 ID,因此最好进行验证,无论输入是如何获得的。

准备就绪

创建报告插件,如第一篇食谱中所述。

如何执行...

我们需要做的就是重写validate方法,加入我们自定义的验证。以下是步骤:

  1. 在我们之前创建的报告类中重写validate方法。

  2. 从请求参数中提取输入参数,这是validate方法的一个参数:

    final String projectid = (String) reqParams.get("projectId");
    

    这里的reqParamsvalidate方法的一个参数:

    public void validate(ProjectActionSupport action, Map reqParams)
    
  3. 检查输入参数的有效性。在我们的示例中,输入参数是projectId。我们可以通过验证是否存在具有给定 ID 的项目来检查其有效性。以下条件在项目 ID 无效时返回 true:

    if (ComponentManager.getInstance().getProjectManager().getProjectObj(pid) == null)
    
  4. 如果参数无效,请向操作添加适当的错误消息:

    action.addError("projectId", "Invalid project Selected");
    

    在这里,我们将字段名传递给 addError 方法,以便错误信息出现在字段上方。

    在这里,你也可以使用国际化来包含适当的错误信息。

  5. 对所有相关的参数添加类似的验证。以下是我们示例中的方法:

    @Override
    public void validate(ProjectActionSupport action, Map reqParams) {
      // Do your validation here if you have any!
       final String projectid = (String) reqParams.get("projectId");
       final Long pid = new Long(projectid);
    
       if (ComponentManager.getInstance().getProjectManager().getProjectObj(pid) == null) {
        action.addError("projectId", "No project with id:"+projectId+" exists!");
      }
      super.validate(action, reqParams);
     }
    
  6. 打包插件并部署!

它是如何工作的...

在报告生成之前,validate 方法会被执行。如果出现任何错误,用户将被带回输入界面,错误信息会高亮显示如下:

它是如何工作的...

这个例子展示了当报告 URL 被篡改以包括无效的项目 ID 12020 时,出现的错误。

参见

  • 编写 JIRA 报告

限制对报告的访问

可以根据预定义的条件限制对 JIRA 报告的访问,例如使报告仅对特定人群可见,或仅在特定项目中显示报告等。让我们快速看看如何为 JIRA 报告编写权限控制代码。

准备就绪

按照第一篇食谱中解释的方法创建报告插件。

如何操作...

在这里,我们只需在报告中实现 showReport 方法。假设我们只想将报告限制为 JIRA 管理员可见。以下是步骤:

  1. 在之前的食谱中创建的报告类中重写 showReport 方法。

  2. 实现逻辑,只有在条件满足时返回 true。在我们的示例中,报告应该仅对 JIRA 管理员可见,因此我们应仅在当前用户是 JIRA 管理员时返回 true

    @Override
    public boolean showReport() {
      User user = ComponentManager.getInstance().getJiraAuthenticationContext().getUser();
      return ComponentManager.getInstance().getUserUtil().getAdministrators().contains(user);
    }
    

    请注意,getJiraAdministrators 方法应在 JIRA v 4.3 及以后版本使用。

  3. 打包插件并部署。

它是如何工作的...

如果用户是管理员,他/她将会在“浏览项目”区域看到报告链接。如果不是,报告链接将不可见。我们可以在 showReport 方法中加入类似的条件,并在返回 true 之前进行评估:

它是如何工作的...

如前截图所示,用户 Test User 不是 JIRA 管理员,因此无法查看 All Issues 报告。

参见

  • 编写 JIRA 报告

可配置的报告对象参数

我们已经了解了如何编写 JIRA 报告,并简要了解了 JIRA 如何让我们配置输入参数。在前面的食谱中,我们介绍了创建 JIRA 报告,并解释了 select 类型的使用。在本食谱中,我们将看到支持的各种属性类型以及如何配置它们的一些示例。

JIRA 支持多种属性类型。你所使用的 JIRA 版本所支持的完整列表可以在 com.atlassian.configurable.ObjectConfigurationTypes 类中找到。对于 JIRA 4.2.*,以下是报告支持的类型:

类型 输入 HTML 类型
string 文本框
long 文本框
hidden 不适用,用户不可见。
date 带日历弹出框的文本框
user 带用户选择器的文本框
text 文本区域
select 选择列表
multiselect 多选列表
checkbox 复选框
filterpicker 过滤器选择器
filterprojectpicker 过滤器或项目选择器
cascadingselect 层级选择列表,依赖于父级选择列表

如何操作...

让我们快速了解每个属性及其用法:

stringstring属性用于创建一个文本框。Java 数据类型是 String。你只需要在这里添加property标签,类型为string

     <property>
       <key>testString</key>
       <name>Test String</name>
       <description>Example String property</description>
       <type>string</type>
       <default>test val</default>
     </property>

每个属性类型,包括string属性,都可以使用default标签填充默认值,如所示。

longlong属性用于创建一个文本框。Java 数据类型再次是 String:

     <property>
       <key>testLong</key>
       <name>Test Long</name>
       <description>Example Long property</description>
       <type>long</type>
     </property>

selectselect属性用于创建一个选择列表。Java 数据类型是 String。我们在之前的示例中见过一个例子。你可以通过两种方式填充选择属性的值:

  1. 使用值生成器类:该类应实现ValuesGenerator接口并返回一个键值对映射。key将是返回给报告类的值,而value是展示给用户的显示值。让我们在这里使用之前示例中的相同例子:

    <property>
      <key>projectId</key>
      <name>Project</name>
      <description>report.allissues.project.description</description>
      <type>select</type>
      <values class="com.jtricks.ProjectValuesGenerator"/>
    </property>
    

    ProjectValuesGenerator实现了如下的getValues()方法:

    public class ProjectValuesGenerator implements ValuesGenerator{
      public Map<String, String> getValues(Map userParams) {
        Map<String, String> projectMap = new HashMap<String, String>();
        List<Project> allProjects = ComponentManager.getInstance().getProjectManager().getProjectObjects();
        for (Project project : allProjects) {
          projectMap.put(project.getId().toString(), project.getName());
        }
        return projectMap;
      }
    }
    
  2. 使用预定义的键值对:以下是一个例子:

        <property>
            <key>testSelect</key>
            <name>Test Select</name>
            <description>Example Select Property</description>
            <type>select</type>
            <values>
              <value>
                  <key>key1</key>
                  <value>Key 1</value>
              </value>
              <value>
                  <key>key2</key>
                  <value>Key 2</value>
              </value>
              <value>
                  <key>key3</key>
                  <value>Key 3</value>
              </value>
            <values>
        </property>
    

multiselectmultiselect属性用于创建一个多选列表。它与选择属性相同。唯一的区别是类型名称是 multiselect。在这里,如果只选择一个值,Java 类型将是 String;如果选择多个值,Java 类型将是字符串数组(String[])!

hiddenhidden属性用于传递一个隐藏值。Java 数据类型是 String:

     <property>
       <key>testHidden</key>
       <name>Test Hidden</name>
       <description>Example Hidden property</description>
       <type>hidden</type>
       <default>test hidden val</default>
     </property>

我们需要使用default标签提供一个值,因为用户无法看到该字段以输入值。

datedate属性用于创建一个日期选择器。Java 数据类型是 String。我们应该将其解析为报告中的Date对象:

     <property>
       <key>testDate</key>
       <name>Test Date</name>
       <description>Example Date property</description>
       <type>date</type>
     </property>

useruser属性用于创建一个用户选择器。Java 数据类型是 String,它将是用户名:

     <property>
       <key>testUser</key>
       <name>Test User</name>
       <description>Example User property</description>
       <type>user</type>
     </property>

texttext属性用于创建一个文本区域。Java 数据类型是 String:

     <property>
       <key>testText</key>
       <name>Test Text Area</name>
       <description>Example Text property</description>
       <type>text</type>
     </property>

checkboxcheckbox属性用于创建一个复选框。Java 数据类型是 String,如果选中,则值为true;如果复选框未选中,则值为null

     <property>
       <key>testCheckbox</key>
       <name>Test Check Box</name>
       <description>Example Checkbox property</description>
       <type>checkbox</type>
     </property>

filterpickerfilterpicker属性用于创建一个过滤器选择器。Java 数据类型是 String,它将保存所选过滤器的 ID:

     <property>
       <key>testFilterPicker</key>
       <name>Test Filter Picker</name>
       <description>Example Filter Picker property</description>
       <type>filterpicker</type>
     </property>

filterprojectpicker:用于创建一个过滤器或项目选择器。Java 数据类型是 String,它将是前缀为 filter(如果选择了过滤器)或 project(如果选择了项目)的 ID:

     <property>
       <key>testFilterProjectPicker</key>
       <name>Test Filter or Project Picker</name>
       <description>Example Filter or Project Picker property</
        description>
       <type>filterprojectpicker</type>
     </property>

cascadingselect:用于基于另一个选择框创建级联选择:

     <property>
       <key>testCascadingSelect</key>
       <name>Test Cascading Select</name>
       <description>Example Cascading Select</description>
       <type>cascadingselect</type>
       <values class="com.jtricks.CascadingValuesGenerator"/>
       <cascade-from>testSelect</cascade-from>
     </property>

这里的级联选择 testCascadingSelect 依赖于名为 testSelect 的选择属性。我们已经看到 testSelect 属性的键/值对。接下来,重要的一点是值生成器类。与其他值生成器类一样,这个类也会生成一个键/值对的映射。

这里,键/值对中的键应该是返回给用户的值。值应该是 ValueClassHolder 类的一个实例,这是一个静态类。ValueClassHolder 类将如下所示:

  private static class ValueClassHolder {
    private String value;
    private String className;

    public ValueClassHolder(String value, String className) {
      this.value = value;
      this.className = className;
    }

    public String getValue() {
      return value;
    }

    public String getClassName() {
      return className;
    }

    public String toString() {
      return value;
    }
  }

ValueClassHolder 中的 value 将是级联选择选项对用户的显示值。className 属性将是父选择项的 key

在我们的示例中,父选择属性是 testSelect。它有三个键 – key1key2key3。因此,getValues() 方法将如下所示:

  public Map getValues(Map arg0) {
    Map allValues = new LinkedHashMap();

    allValues.put("One1", new ValueClassHolder("First Val1", "key1"));
    allValues.put("Two1", new ValueClassHolder("Second Val1", "key1"));
    allValues.put("Three1", new ValueClassHolder("Third Val1", "key1"));
    allValues.put("One2", new ValueClassHolder("First Val2", "key2"));
    allValues.put("Two2", new ValueClassHolder("Second Val2", "key2"));
    allValues.put("One3", new ValueClassHolder("First Val3", "key3"));

    return allValues;
  }  

如果你举一个单独的例子,比如 allValues.put("One1", new ValueClassHolder("First Val1", "key1")),当选择列表中的 key1 被选中时,它将有一个键值对 One1/First Val1

在选择适当的值后,它们可以在报告类中被检索,如以下代码所示:

    final String testString = (String) reqParams.get("testString");
    final String testLong = (String) reqParams.get("testLong");
    final String testHidden = (String) reqParams.get("testHidden");
    final String testDate = (String) reqParams.get("testDate");
    final String testUser = (String) reqParams.get("testUser");
    final String testText = (String) reqParams.get("testText");
    final String[] testMultiSelect = (String[]) reqParams.get("testMultiSelect");
    final String testCheckBox = (String) reqParams.get("testCheckBox");
    final String testFilterPicker = (String) reqParams.get("testFilterPicker");
    final String testFilterProjectPicker = (String) reqParams.get("testFilterProjectPicker");
    final String testSelect = (String) reqParams.get("testSelect");
    final String testCascadingSelect = (String) reqParams.get("testCascadingSelect");

特别需要提到的是 filterprojectpicker。如果选择了 ID 为 10000 的过滤器,则值为 filter-10000。如果选择了 ID 为 10000 的项目,则值为 project-10000

工作原理...

当报告输入界面呈现给用户时,插件描述符中提到的属性将转换为适当的 HTML 元素,如前所述。然后,我们可以在报告类中检索它们的值并进行处理,以生成报告。

以下两个截图展示了这些属性在输入界面上的显示:

工作原理...工作原理...

如果你在报告类中将提取的值打印到控制台,它将如下所示:

工作原理...

希望这能给你一个关于如何在 JIRA 报告中使用可配置参数的清晰概念。

另见

  • 编写 JIRA 报告

在 JIRA 中创建饼图

正如我们在前面的例子中已经看到的那样,JIRA 自带了一些内置报告。它还允许我们使用报告插件模块编写自己的报告。在 JIRA 中,吸引大量用户的一种报告是饼图。尽管现有的 JIRA 饼图报告非常适合它的用途,但有时我们也需要编写自己的饼图。

在 JIRA 中编写饼图非常简单,因为 JIRA 已经支持 JFreeChart 并且具有许多实用类,这些类可以完成大部分创建这些图表的工作。在这个实例中,我们将展示如何利用 Atlassian 实用类编写一个简单的饼图。

准备开始…

使用 Atlassian Plugin SDK 创建一个插件骨架。

如何操作...

让我们尝试创建一个非常简单的饼图,不包含任何业务逻辑。为了简化操作并集中精力在饼图上,我们可以做一个没有任何输入参数并且只有 HTML 视图的报告。以下是实现这一目标的步骤:

  1. 在插件描述符中添加报告插件模块:

    <report key="pie-chart" name="Pie Chart" class="com.jtricks.PieChart">
      <description>Sample Pie Chart</description>
      <label>Eaxmple Pie Chart</label>
      <resource type="velocity" name="view" location="templates/pie/pie-chart.vm" />
    </report>
    

    它只包含一个类和一个用于 HTML 视图的 velocity 模板。

  2. 创建报告类。像往常一样,它应该实现 AbstractReport 类。我们在这里所做的就是用我们通过自定义 PieChartGenerator 类创建的饼图参数填充 velocity 模板。

    下面是 generateReportHtml 的样子:

    public String generateReportHtml(ProjectActionSupport action, Map reqParams) throws Exception {
      final Map<String, Object> params = new HashMap<String, Object>();
      params.put("report", this);
      params.put("action", action);
      params.put("user", authenticationContext.getUser());
    
      final Chart chart = new JTricksPieChartGenerator().generateChart(authenticationContext.getUser(),REPORT_IMAGE_WIDTH, REPORT_IMAGE_HEIGHT);
    
      params.putAll(chart.getParameters());
      return descriptor.getHtml("view", params);
    }
    

    Chart 类是 Atlassian 类,类型为 com.atlassian.jira.charts.Chart。饼图的创建业务逻辑在自定义工具类 JTricksPieChartGenerator 中完成,我们接下来将看到它。

  3. 创建 JTricksPieChartGenerator 工具类,用于生成饼图。

    这里是饼图创建的业务逻辑,因此我们将详细查看这些内容:

    • 创建 DefaultPieDataset,它将作为饼图的数据集。这是一个 JFreeChart 类,Java 文档可以在 www.jfree.org/jfreechart/api/javadoc/org/jfree/data/general/DefaultPieDataset.html 中找到。

      DefaultPieDataset dataset = new DefaultPieDataset();
      
    • 填充 dataset 中的值:

      dataset.setValue("One", 10L);dataset.setValue("Two", 15L);
      

      在这个示例中,我们仅填充了两个键值对,分别是 namenumber value。这是用于生成饼图的数据。当我们生成自定义图表时,我们应该将其替换为我们感兴趣的合适数据。

    • 获取一个 i18nBean。这是 Atlassian 工具类中所需的:

      final I18nBean i18nBean = new I18nBean(remoteUser);
      
    • 创建图表:

      final ChartHelper helper = new PieChartGenerator(dataset, i18nBean).generateChart();helper.generate(width, height);
      

      在这里我们使用 com.atlassian.jira.charts.jfreechart.PieChartGenerator 类,通过我们刚创建的 dataset 和 i18nBean 生成图表。确保调用生成方法,如前面的代码片段所示。

    • 使用从生成的 ChartHelper 中获取的所有必需参数填充一个 map,并返回一个 Chart 对象,如下所示:

      params.put("chart", helper.getLocation());
      params.put("chartDataset", dataset);
      params.put("imagemap", helper.getImageMap());
      params.put("imagemapName", helper.getImageMapName());
      
      return new Chart(helper.getLocation(), helper.getImageMap(), helper.getImageMapName(), params);
      

      你可以添加所有可用的参数,但我们将其限制为最基本的参数。params.putAll(chart.getParameters()) 在报告类中将填充所有这些参数到 velocity 上下文中。

    • generateChart 方法现在的样子如下:

            public Chart generateChart(User remoteUser, int width, int 
              height) {
            try {
              final Map<String, Object> params = new HashMap<String, 
                Object>();
              // Create Dataset
              DefaultPieDataset dataset = new DefaultPieDataset();
      
              dataset.setValue("One", 10L);
              dataset.setValue("Two", 15L);
      
              final I18nBean i18nBean = new I18nBean(remoteUser);
      
              final ChartHelper helper = new PieChartGenerator(dataset,       i18nBean).generateChart();
              helper.generate(width, height);
      
              params.put("chart", helper.getLocation());
              params.put("chartDataset", dataset);
              params.put("imagemap", helper.getImageMap());
              params.put("imagemapName", helper.getImageMapName());
              return new Chart(helper.getLocation(), helper.
                getImageMap(), helper.getImageMapName(), params);
      
            } catch (Exception e) {
              e.printStackTrace();
              throw new RuntimeException("Error generating chart", e);
            }
          }
      
  4. 使用我们在报告类中填充的上下文,创建 HTML 视图的 velocity 模板。在我们的示例中,模板是 templates/pie/pie-chart.vm。它看起来像下面这段代码:

    Sample Chart: <br><br>
    <table width="100%" class="report">
      <tr>
        <td>
          #if ($chart)
             #if ($imagemap)
              $imagemap
            #end
            <p class="report-chart">
              <img src='$baseurl/charts?filename=$chart' border='0' #if ($imagemap) usemap="\#$imagemapName" #end/>
            </p>
          #end
        </td>
      </tr>
    </table>
    

    这里我们展示我们创建的图表。图表可以通过 URL $baseurl/charts?filename=$chart 获取,其中 $chart 是由辅助类生成的位置。我们之前已在上下文中填充了这个信息。

  5. 打包插件,部署并测试!

它是如何工作的...

简而言之,我们在这里需要做的是创建DefaultPieDataset,其他一切将由 JIRA 为你完成。generateChart方法可能会根据我们要创建的报告的复杂性需要更多的参数。例如:startDateendDate等。然后,数据集将使用这些参数创建,而不是我们硬编码的值!

在我们的示例中,图表显示如下:

如何工作...

还有更多...

查看com.atlassian.jira.charts.jfreechart包下的其他帮助类,例如StackedBarChartGeneratorHistogramChartGeneratorCreatedVsResolvedChartGenerator等,用于生成其他类型的图表!

另见

  • 编写 JIRA 报告

编写 JIRA 4 小工具

小工具是 JIRA 报告功能的一大飞跃!JIRA 现在是一个 OpenSocial 容器,允许用户将有用的小工具(包括 JIRA 自带的和第三方的)添加到仪表板中。同时,针对 JIRA 编写的小工具也可以添加到其他容器中,如 iGoogle、Gmail 等!

在本教程中,我们将编写一个非常简单的小工具,它会显示“Hello from JTricks”。通过保持内容简单,我们可以更专注于编写小工具的工作!

在开始编写小工具之前,理解 JIRA 小工具的关键组成部分可能是值得的:

  1. 小工具 XML 是 JIRA 小工具中最重要的部分。它包含小工具的规范,并包括以下内容:

    • 小工具特点。它包括标题、描述、作者姓名等

    • 截图和缩略图。请注意,截图不会在 Atlassian 容器中使用,如 JIRA 或 Confluence。如果我们希望它们在其他 OpenSocial 容器中使用,可以选择性地添加它们。

    • 小工具容器必须为小工具提供的必需功能

    • 用户偏好,由小工具用户配置

    • 使用 HTML 和 JavaScript 创建的小工具内容

  2. 截图和缩略图将在预览时以及从容器中选择小工具时使用。

  3. 用于小工具国际化的i18n属性文件

  4. 可选的 CSS 和 JavaScript 文件,用于渲染小工具内容部分的显示

我们将在本教程中逐一介绍它们。

准备工作

使用 Atlassian Plugin SDK 创建一个骨架插件。

如何做...

以下是编写我们第一个小工具的步骤,它会显示来自 JTricks 的问候!

  1. 使用小工具模块和我们小工具所需的资源修改插件描述符:

    • 在插件描述符中添加Gadget模块:

      <gadget key="hello-gadget" name="Hello Gadget" location="hello-gadget.xml"><description>Hello Gadget! </description>
      </gadget>
      

      如你所见,这有一个独特的key,指向小工具 XML 的location!你可以在atlassian-plugin.xml文件中定义任意多个小工具,但在我们的示例中,我们保持使用前面的定义。

    • 在插件描述文件中包括缩略图、截图图像和可下载资源。我们在上一章中已经看到过这一点,更多内容可以在confluence.atlassian.com/display/JIRADEV/Downloadable+Plugin+Resources上了解。在我们的示例中,资源作为以下内容添加到插件描述文件中:

      <resource type="download" name="screenshot.png" location="/images/screenshot.png"/>
      <resource type="download" name="thumbnail.png" location="/images/thumbnail.png"/>
      

      位置是相对于插件中的src/main/resources文件夹。正如前面所述,截图是可选的。

  2. 添加将用于小工具的i18n属性文件,也作为可下载资源:

    <resource type="download" name="i18n/messages.xml" location="i18n/messages.xml">
      <param name="content-type" value="text/xml; charset=UTF-8"/>
    </resource>
    

    现在,atlassian-plugin.xml将如下所示:

    <atlassian-plugin key="com.jtricks.gadgets" name="Gadgets Plugin" plugins-version="2">
        <plugin-info>
          <description>Gadgets Example</description>
          <version>2.0</version>
          <vendor name="JTricks" url="http://www.j-tricks.com/" />
        </plugin-info>
        <gadget key="hello-gadget" name="Hello Gadget" location="hello-gadget.xml">
          <description>Hello Gadget!</description>
        </gadget>
    
        <resource type="download" name="screenshot.png" location="/images/screenshot.png"/>
        <resource type="download" name="thumbnail.png" location="/images/thumbnail.png"/>
    
        <resource type="download" name="i18n/messages.xml" location="i18n/messages.xml">
          <param name="content-type" value="text/xml; charset=UTF-8"/>
        </resource>
      </atlassian-plugin>
    
  3. src/main/resources/images文件夹下添加截图和缩略图图像。缩略图图像的大小应为 120 x 60 像素。

  4. i18n属性文件添加到src/main/resources/i18n文件夹下。我们在messages.xml中定义的文件名。

    该文件是一个 XML 文件,包含在messagebundle标签中。文件中的每个属性作为 XML 标签输入,如下所示:

    <msg name="gadget.title">Hello Gadget</msg>
    

    msg标签有一个name属性,表示属性,且相应的值被包含在msg标签中。我们在示例中使用了三个属性,整个文件如下所示:

    <messagebundle>
      <msg name="gadget.title">Hello Gadget</msg>
      <msg name="gadget.title.url">http://www.j-tricks.com</msg>
      <msg name="gadget.description">Example Gadget from J-Tricks</msg>
    </messagebundle>
    
  5. 编写小工具 XML。

    小工具 XML 的根元素是Module。它下面主要有三个元素——ModulePrefsUserPrefContent。我们将在本示例中介绍每个元素。有关小工具规范的所有属性、元素和其他详细信息,可以在confluence.atlassian.com/display/GADGETDEV/Creating+your+Gadget+XML+Specification阅读。

    • 编写ModulePrefs元素。该元素包含有关小工具的信息。它还具有两个子元素——RequireOptional,用于定义小工具所需或可选的功能。

      以下是我们示例中ModulePrefs元素在填充所有属性后样子的示例:

      <ModulePrefs title="__MSG_gadget.title__" 
                   title_url="__MSG_gadget.title.url__" 
                   description="__MSG_gadget.description__" 
                   author="Jobin Kuruvilla" 
                   author_email=jobinkk@gmail.com
                   screenshot='#staticResourceUrl("com.jtricks.gadgets:hello-gadget", "screenshot.png")' 
                   thumbnail='#staticResourceUrl("com.jtricks.gadgets:hello-gadget", "thumbnail.png")' height="150"  >
      </ModulePrefs>
      

      如您所见,它包含了诸如title(标题)、title URL(小工具标题将链接到的 URL)、description(描述)、author(作者)、name(名称)和email(电子邮件)、小工具的height(高度)、截图和缩略图图像的 URL 等信息。

      任何以__MSG_开头并以__结尾的内容,都是从i18n属性文件中引用的属性。

      小工具的height(高度)是可选的,默认值为 200。图像使用#staticResourceUrl进行引用,其中第一个参数是完全限定的小工具模块键,形式为${atlassian-plugin-key}:${module-key}。在我们的示例中,插件键是com.jtricks.gadgets,模块键是hello-gadget

    • ModulePrefs中添加可选的小工具目录功能。目前仅在 JIRA 中支持:

      <Optional feature="gadget-directory">
        <Param name="categories">
          Other
        </Param>
      </Optional>
      
      

      在示例中,我们将类别添加为Other

      类别支持的其他值包括:JIRAConfluenceFishEyeCrucibleCrowdCloverBambooAdminChartsExternal Content

    你可以通过在 Param 元素中添加类别,每行一个,来将小工具添加到多个类别中。

    • 如果有需要,包含 Required 功能,放在 XML 标签 require 下。完整的支持功能列表可以在 confluence.atlassian.com/display/GADGETDEV/Including+Features+into+your+Gadget 找到。

    • 添加 Locale 元素以指向 i18n 属性文件:

      <Locale messages="__ATLASSIAN_BASE_URL__/download/resources/com.jtricks.gadgets/i18n/messages.xml"/>
      

      在这里,属性 __ATLASSIAN_BASE_URL__ 会在小工具渲染时自动被替换为 JIRA 配置的基础 URL。属性文件的路径是 __ATLASSIAN_BASE_URL__/download/resources/com.jtricks.gadgets,其中 com.jtricks.gadgets 是 Atlassian 插件的密钥。XML 文件的路径 /i18n/messages.xml 是之前在资源模块中定义的。

    • 如果需要,使用 UserPref 元素添加用户偏好设置。在本例中,我们将省略这一部分,因为“Hello Gadget”不需要用户输入。

    • 添加小工具的 Content。这是使用 HTML 和 JavaScript 渲染小工具的地方。在我们的示例中,我们只需要提供静态文本“Hello From JTricks”,这是相当简单的。

    整个内容被包装在 < ![CDATA[]]> 之间,以便它们不会被当作 XML 标签处理。以下是我们示例中的表现形式:

    <Content type="html" view="profile">
      < ![CDATA[ Hello From JTricks ]]>
    </Content>
    

    我们的小工具的 XML 文件现在已经准备好,格式如下所示:

    <?xml version="1.0" encoding="UTF-8" ?>
    <Module>
      <ModulePrefs title="__MSG_gadget.title__" 
                   title_url="__MSG_gadget.title.url__" 
                   description="__MSG_gadget.description__" 
                   author="Jobin Kuruvilla" 
                   author_email=jobinkk@gmail.com 
                   screenshot='#staticResourceUrl("com.jtricks.gadgets:hello-gadget", "screenshot.png")' 
                   thumbnail='#staticResourceUrl("com.jtricks.gadgets:hello-gadget", "thumbnail.png")' height="150" >
        <Optional feature="gadget-directory">
          <Param name="categories">
            Other
          </Param>
        </Optional>
        <Locale messages="__ATLASSIAN_BASE_URL__/download/resources/com.jtricks.gadgets/i18n/messages.xml"/>
      </ModulePrefs>
      <Content type="html" view="profile">
        < ![CDATA[ Hello From JTricks ]]>
       </Content>
    </Module>
    
  6. 打包插件,部署并测试。

工作原理...

一旦插件部署完成,我们需要将小工具添加到 JIRA 仪表盘中。以下是它在 添加小工具 页面上的显示方式。请注意,缩略图是我们插件中的那个,并且它出现在 其他 部分:

工作原理...

添加后,它在 仪表盘 部分显示如下:

工作原理...

还有更多...

我们可以通过添加更多 HTML 或小工具偏好设置来修改小工具的外观和感觉!例如,<font color="red">Hello From JTricks</font> 会使其显示为红色。

我们可以使用动态高度功能调整小工具的大小。我们应该在 ModulePrefs 元素下添加以下内容:

<Require feature="dynamic-height"/>

每当内容重新加载时,我们应该调用 gadgets.window.adjustHeight();。例如,我们可以在窗口加载事件中这样做,如下所示:

  <script type="text/javascript" charset="utf-8">
    function resize()
    {
      gadgets.window.adjustHeight();
    }
    window.onload=resize;
  </script>

此时,小工具的 gadget xml 文件应该如下所示:

<?xml version="1.0" encoding="UTF-8" ?>
<Module>
    <ModulePrefs title="__MSG_gadget.title__"
                 title_url="__MSG_gadget.title.url__"
                 description="__MSG_gadget.description__"
                 author="Jobin Kuruvilla"
                 author_email="jobinkk@gmail.com"
                 screenshot='#staticResourceUrl("com.jtricks.gadgets:hello-gadget", "screenshot.png")'
                 thumbnail='#staticResourceUrl("com.jtricks.gadgets:hello-gadget", "thumbnail.png")'
     height="150">
        <Optional feature="gadget-directory">
            <Param name="categories">Other</Param>
        </Optional>
  <Require feature="dynamic-height"/>
  <Locale messages="__ATLASSIAN_BASE_URL__/download/resources/com.jtricks.gadgets/i18n/messages.xml"/>
    </ModulePrefs>
    <Content type="html" view="profile">
        < ![CDATA[
  <script type="text/javascript" charset="utf-8">
      function resize() 
      {
    gadgets.window.adjustHeight();
      }
  window.onload=resize;
  </script>
  Hello From JTricks
        ]]>
    </Content>
</Module>

小工具现在应该显示如下:

还有更多...

请注意,大小已经调整为正好适应文本!

从小工具调用 REST 服务

在前一个例子中,我们了解了如何编写一个带有静态内容的小工具。在本例中,我们将看看如何创建一个带有动态内容的小工具,或者说是来自 JIRA 服务器的数据。

JIRA 使用 REST 服务在小工具和服务器之间进行通信。我们将在接下来的章节中学习如何编写 REST 服务。在本章中,我们将使用现有的 REST 服务。

正在准备中

创建Hello Gadget,如上一章节所述。

如何操作...

让我们考虑对现有的Hello Gadget进行简单修改,以理解如何从小工具中调用 REST 服务的基础知识。我们将尝试通过从服务器获取用户详细信息来问候当前用户,而不是显示静态文本:Hello From JTricks

JIRA 提供了一些内置的 REST 方法,其中之一是获取当前用户的详细信息。该方法可以通过 URL 访问:/rest/gadget/1.0/currentUser。我们将使用该方法来获取当前用户的全名,并将其显示在小工具的问候语中。如果用户的名字是Jobin Kuruvilla,小工具将显示消息:Hello, Jobin Kuruvilla

由于我们只是修改了小工具的内容,所以唯一需要修改的是小工具 XML 文件,即我们的示例中的 hello-gadget.xml。只需要修改 Content 元素,它现在将调用 REST 服务并呈现内容。

以下是步骤:

  1. 包含常见的 Atlassian 小工具资源:

    #requireResource("com.atlassian.jira.gadgets:common")
    #includeResources()
    

    #requireResource 将 JIRA 小工具的 JavaScript 框架引入小工具的上下文中。#includeResources 会写出资源的 HTML 标签。更多详情请查看 confluence.atlassian.com/display/GADGETDEV/Using+Web+Resources+in+your+Gadget

  2. 构建一个小工具对象,如下所示:

    var gadget = AJS.Gadget
    

    小工具对象有四个顶级选项:

    • baseUrl: 用于传递基础 URL 的选项。此选项是必需的,我们在这里使用 __ATLASSIAN_BASE_URL__,它将渲染为 JIRA 的基础 URL。

    • useOauth: 一个可选的参数。用于配置身份验证类型,必须是一个 URL。通常使用 /rest/gadget/1.0/currentUser

    • config: 另一个可选参数。仅在小工具有任何配置选项时使用。

    • view: 用于定义小工具的视图。

      在我们的示例中,我们没有使用身份验证或任何配置选项。我们只会使用 baseUrlview 选项。以下是如何使用 JavaScript 创建小工具:

          <script type="text/javascript">
            (function () {
                  var gadget = AJS.Gadget({
                    baseUrl: "__ATLASSIAN_BASE_URL__",
                    view: {
                  ...
                }
                  });
            })();
          </script>
      
  3. 填充小工具视图。

    view 对象具有以下属性:

    • enableReload: 可选的。用于定期重新加载小工具。

    • onResizeReload: 可选的。用于在浏览器调整大小时重新加载小工具。

    • onResizeAdjustHeight: 可选的,并与 dynamic-height 功能一起使用。当浏览器调整大小时,这将调整小工具的高度。

    • template: 创建实际视图。

    • args:一个对象数组或返回对象数组的函数。它有两个属性。Key——用于在模板中访问数据;ajaxOptions——一组用于连接服务器并检索数据的请求选项。

    在我们的示例中,我们将使用templateargs属性来渲染视图。首先,让我们看一下args,因为我们在template中使用了从这里获取的数据。args将如下所示:

        args: [{
          key: "user",
          ajaxOptions: function() {
            return {
              url: "/rest/gadget/1.0/currentUser"
            };
          }
        }]
    
    

    如你所见,我们调用了/rest/gadget/1.0/currentUser方法,并使用user键来引用我们在渲染视图时获取的数据。ajaxOptions使用了 jQuery 的 Ajax 选项,详细信息可以在api.jquery.com/jQuery.ajax#options找到。

    user键现在将保存来自 REST 方法的用户详细信息,如下所示:

    UserPref name="displayName" datatype="hidden" default_value="true
    {"username":"jobinkk","fullName":"Jobin Kuruvilla","email":"jobinkk@gmail.com"}
    

    template函数现在将使用这个args对象(如前所定义)及其keyuser)来渲染视图,如下所示:

        template: function(args) {
          var gadget = this;
    
          var userDetails = AJS.$("<h1/>").text("Hello, "+args.user["fullName"]);     
          gadget.getView().html(userDetails);
        }
    
    

    这里,args.user["fullName"]将从 REST 输出中获取用户的fullName。用户名或电子邮件也可以以类似的方式获取。

    AJS.$将构建视图为<h1>Hello, Jobin Kuruvilla</h1>,其中Jobin Kuruvilla是获取到的fullName

    整个Content部分将如下所示:

        <Content type="html" view="profile">
              < ![CDATA[
                    #requireResource("com.atlassian.jira.gadgets:common")
              #includeResources()
    
              <script type="text/javascript">
          (function () {
                var gadget = AJS.Gadget({
                    baseUrl: "__ATLASSIAN_BASE_URL__",
                    view: {
                    template: function(args) {
                     var gadget = this;       
                     var userDetails = AJS.$("<h1/>").text("Hello, "+args.user["fullName"]);       
                     gadget.getView().html(userDetails);
                    },
                    args: [{
                    key: "user",
                    ajaxOptions: function() {
                      return {
                        url: "/rest/gadget/1.0/currentUser"
                      };
                  }
                  }]
              }
              });
          })();
              </script>
              ]]>
            </Content>
    
  4. 打包小部件并部署它。

如何工作...

修改小部件 XML 后,小部件现在将显示如下方法:

如何工作...

另见

  • 编写 JIRA 4 小部件

在小部件中配置用户偏好

在前两个食谱中,我们展示了如何从静态内容和动态内容中创建小部件。在这个食谱中,我们将更进一步,根据用户输入显示小部件内容。

用户将在创建小部件时配置它,或者稍后修改它,且小部件内容将根据配置参数有所不同。

正在准备中...

创建Hello Gadget,并填充动态内容,正如前面的食谱所描述。

如何操作...

在这个食谱中,我们将让用户选择是否在问候消息中显示姓名。小部件将有一个名为displayName的属性。如果它设置为true,则小部件将显示用户名,问候消息将为Hello, Jobin Kuruvilla。如果displayName设置为 false,问候消息将是Hello

以下是配置用户偏好的步骤:

  1. ModulePrefs元素下包含setprefsviews功能:

    <Require feature="setprefs" /><Require feature="views" />
    

    setprefs用于持久化用户偏好,而views决定当前用户是否可以编辑偏好设置。

  2. ModulePrefs下包含小部件、common语言环境,以及我们的自定义Locale元素:

    #supportedLocales("gadget.common")
    

    这是确保小部件配置语言正确的必要步骤。

  3. 包含所需的UserPref元素。此元素定义了各种用户偏好。该元素支持以下字段:

    • name:必填项。用户偏好的名称。其值可以通过 gadget.getPref("name") 获取。

    • display_name:字段的显示名称。默认情况下,它与名称相同。

    • urlparam:可选字符串,作为内容 type="url" 的参数名称传递。

    • datatype:字段的数据类型。有效选项包括:stringboolenumhiddenlist。默认值是 string。

    • required:标记字段为必填项。默认值是 false。

    • default_value:设置默认值。

    在我们的示例中,我们按如下方式添加 displayName 属性:

        <UserPref name="displayName" datatype="hidden" default_value="true"/>
    

    该字段被标记为hidden,因此它不会出现在 OpenSocial 小部件的配置表单中!

  4. 修改 AJS.Gadget 的创建以包含 config 属性。config 通常是以下形式:

    ...
    config: {
        descriptor: function(){...},
        args: {Function, Array}
    },
    ... 
    

    这里,descriptor 是一个返回新配置描述符的函数。args 是一个对象数组或返回类似 view 的函数。

    在我们的示例中,我们定义了一个函数来返回包含 displayName 属性配置详情的描述符。它如下所示:

    config: {
      descriptor: function (args) {
        var gadget = this;
        return  {
          fields: [
            {
              userpref: "displayName",
              label: gadget.getMsg("property.label"),
              description:gadget.getMsg("property.description"),
              type: "select",
              selected: gadget.getPref("displayName"),
              options:[
                {
                   label:"Yes",
                  value:"true"
                },
                {
                  label:"No",
                  value:"false"
                }
              ]
             }
          ]
         };
      }
    }
    

    这里只有一个字段:displayName。它是 select 类型,并且有一个 labeldescription,这两者都是通过 i18n 属性文件,使用 gadget.getMsg 方法填充的。Selected 属性被填充为当前值 – gadget.getPref("displayName")Options 作为数组提供,如前面的代码片段所示。

    关于各种其他字段类型及其属性的更多详细信息可以在 confluence.atlassian.com/display/GADGETDEV/Field+Definitions 上找到。

  5. 将新的 i18n 属性添加到消息包中:

    <msg name="property.label">Display Name?</msg><msg name="property.description">Example Property from J-Tricks</msg>
    
  6. 包括 UserPrefisConfigured

    <UserPref name="isConfigured" datatype="hidden" default_value="false"/>
    

    用户偏好每次小部件加载时都会设置,我们使用这个专门设计的属性来防止这种情况。

    使用此属性时,应该在 config descriptor 下添加 AJS.gadget.fields.nowConfigured() 作为额外字段。

  7. 修改视图以根据配置的属性显示用户名。

    template 函数修改如下:

    if (gadget.getPref("displayName") == "true")
      var userDetails = AJS.$("<h1/>").text("Hello, "+args.user["fullName"]);
    } else {
      var userDetails = AJS.$("<h1/>").text("Hello!");
     }  
    

    如你所见,配置的属性是通过 gadget.getPref("displayName") 获取的。如果它为 true,则使用用户名。

    现在整个 Content 部分看起来像以下代码行:

    <Content type="html" view="profile">
    < ![CDATA[#requireResource("com.atlassian.jira.gadgets:common")
    #includeResources()
    <script type="text/javascript">
          (function () {
    var gadget = AJS.Gadget({
    	baseUrl: "__ATLASSIAN_BASE_URL__",
    	config: {
    descriptor: function (args) {
    var gadget = this;
    return  {
    fields: [
    {
    userpref: "displayName",
    label: gadget.getMsg("property.label"),
    description:gadget.getMsg("property.description"),
    type: "select",
    selected: gadget.getPref("displayName"),
    options:[
                        {
    label:"Yes",
    value:"true"
                 },
                        {
    label:"No",
    value:"false"
                        }
                      ]
    }, 
    AJS.gadget.fields.nowConfigured()
                    ]
    };
    }
    	},
    	view: {
    template: function(args) {
    var gadget = this;       
    if (gadget.getPref("displayName") == "true")
                  {
    varuserDetails = AJS.$("<h1/>").text("Hello, "+args.user["fullName"]);
                  } else {
    varuserDetails = AJS.$("<h1/>").text("Hello!");
                  }       
    gadget.getView().html(userDetails);
    },
    args: [{
    key: "user",
    ajaxOptions: function() {
    return {
    url: "/rest/gadget/1.0/currentUser"
                    };
                  }
                }]
    	}
    	});
          })();
    </script>
    ]]>
    </Content>
    
  8. 打包小部件并部署它。

它是如何工作的...

一旦添加了用户可配置的属性,创建小部件时将要求用户配置 displayName 属性,如下所示。默认值将是 true(标签:是),正如我们所配置的那样。

它是如何工作的...

当选择时,它会显示为:

它是如何工作的...

如果现在点击小部件选项,你可以看到编辑选项,如下图所示:

它是如何工作的...

点击编辑时出现如下截图:

它是如何工作的...

选择时,消息将不显示用户名,如下图所示:

它是如何工作的...

还有更多...

JIRA 小工具中最受欢迎的用户偏好之一,因此值得特别提及的是它能够在配置的时间间隔内自动刷新。JIRA 有一个预定义的功能,可以帮助我们实现这一点。

实现此功能只需要做几件事:

  1. 添加 refresh 用户偏好:

    <UserPref name="refresh" datatype="hidden" default_value="false"/>
    
  2. view 中包含 enableReload: true 属性:

    view: {
      enableReload: true,
       template: function(args) {
        ...
      },
      args: [{
        ...
       }]
    }
    

现在你将在小工具属性中看到一个额外的刷新操作,如下图所示:

还有更多...

这可以用于随时刷新小工具。

点击编辑时,可以选择自动刷新间隔,如下图所示:

还有更多...

另见

  • 编写 JIRA 4 小工具

  • 从小工具调用 REST 服务

访问 JIRA 之外的小工具

我们已经看过如何编写小工具并将其添加到 JIRA 仪表板上。但我们是否已经充分利用了 OpenSocial 小工具的所有优势?如何将它们添加到其他 OpenSocial 容器中,例如 Gmail 或 iGoogle?

在这个教程中,我们将看到如何将小工具添加到 Gmail。对于其他容器,过程也是非常相似的。

如何操作...

以下是将小工具添加到 Gmail 的快速步骤:

  1. 确定我们要添加的小工具的 URL。我们可以从 JIRA 小工具目录中找到这个 URL,如下图所示。在示例中,我们选择添加收藏的过滤器小工具:如何操作...

  2. 进入Gmail | 设置 | 小工具。输入 URL,如下图所示:如何操作...

    请注意,这是不同容器之间唯一不同的步骤。我们需要在每个容器的适当位置输入这个 URL。

  3. 添加后,小工具将在设置中显示,如下图所示:如何操作...

  4. 小工具现在应该可以在 Gmail 侧边栏中的小工具列表下使用。保存配置。在我们的示例中,我们需要选择是否显示问题的计数以及刷新间隔。

    请参见下一张截图,了解它在 Gmail 中的显示效果。

  5. 小工具现在显示没有结果,因为我们没有使用正确的用户名/密码连接到 JIRA。编辑小工具设置,你将看到一个选项,登录并批准,该选项允许你登录到 JIRA 实例并批准显示在 Gmail 中的数据获取:如何操作...

  6. 批准访问,如以下截图所示。现在小工具应该显示结果:如何操作...

它是如何工作的...

它的工作方式与在 JIRA 仪表板中的行为完全相同。该小工具将通过 REST API 与 JIRA 通信,并使用 HTML 和 JavaScript 代码渲染数据,这些代码位于小工具 XML 文件的 Content 元素下的 view 部分。

另见

  • 编写 JIRA 4 小工具

  • 从小工具调用 REST 服务

第六章:JIRA 搜索的强大功能

在本章中,我们将涵盖:

  • 编写 JQL 函数

  • 清理 JQL 函数

  • 添加搜索请求视图

  • 使用快速搜索进行智能查询

  • 插件中的搜索

  • 在插件中解析 JQL 查询

  • 直接链接到搜索查询

  • 编程方式进行索引和取消索引

  • 程序化管理过滤器

介绍

JIRA 以其强大的搜索功能而著名。它允许我们以令人印象深刻的方式扩展这些功能!在本章中,我们将探讨定制 JIRA 的各种搜索功能,例如 JQL、插件中的搜索、管理过滤器等。

在我们开始之前,值得关注 JIRA 4 中的一项重要增强功能——JQLJIRA 查询语言)。JQL 提供了先进的搜索功能,用户可以利用这些功能在 JIRA 实例中搜索问题,然后利用问题导航器的所有功能。

除了之前的搜索功能,现在称为简单搜索,JQL 或高级搜索引入了对逻辑运算的支持,包括 AND、OR、NOT、NULL 和 EMPTY。它还引入了一组 JQL 函数,可以有效地根据预定义标准进行搜索。

JQL 是一种结构化查询语言,让我们使用类似 SQL 的简单语法查找问题。它之所以简单,是因为它的自动完成特性,并且保存查询历史,便于轻松导航到最近的搜索。正如 Atlassian 所说:

“JQL 允许你使用标准的布尔运算符和通配符来执行复杂的搜索,包括模糊搜索、邻近搜索和空字段搜索。它甚至支持可扩展的函数,允许你定义像“CurrentUser”或“LastSprint”这样的自定义表达式进行动态搜索。”

高级搜索中的查询由字段、接着是操作符、然后是函数组成。要查找项目中的所有问题,我们可以使用:

project = "TEST"

project 是字段,= 是操作符,TEST 是值。

同样,我们可以使用以下方式找到分配给当前用户的所有问题:

assignee = currentUser()

assignee 是字段,= 是操作符,currentUser() 是一个 JQL 函数。

此时,JQL 不支持在单个查询中比较两个字段或两个函数。但我们可以使用逻辑运算符和关键字来引入更多控制,如下所示:

project = "TEST" AND assignee = currentUser()

该查询将显示在项目 TEST 中且当前用户为 assignee 的问题。有关高级搜索的更详细说明,以及完整的关键字、操作符、字段和函数参考,可以在 confluence.atlassian.com/display/JIRA/Advanced+Searching 找到。

编写 JQL 函数

如我们所见,JQL 函数允许我们定义自定义表达式或搜索器。JIRA 有一组内置的 JQL 函数,详情可以参考confluence.atlassian.com/display/JIRA/Advanced+Searching#AdvancedSearching-FunctionsReference。在本教程中,我们将学习如何编写一个新的 JQL 函数。

JQL 函数提供了一种方法,允许在 JQL 查询中根据运行时的参数值进行计算。它接受可选参数,并根据运行时的参数返回结果。

在我们的例子中,假设创建一个名为projects()的函数,它可以接收一个项目键的列表,并返回所有该项目中的问题。例如:

project in projects("TEST", "DEMO")

它等同于:

project in ("TEST","DEMO") and also to project = "TEST" OR project = "DEMO"

我们仅为本教程引入这个新函数。

准备工作

使用 Atlassian 插件 SDK 创建一个骨架插件。

如何实现...

JIRA 使用JQL 函数模块将新的 JQL 函数添加到高级搜索中。以下是我们示例的逐步过程:

  1. 修改插件描述符以包括 JQL 函数模块:

    <jql-function key="jql-projects" name="Projects Function" class="com.jtricks.ProjectsFunction">
      <!--The name of the function-->
      <fname>projects</fname>
    
      <!--Whether this function returns a list or a single value-->
      <list>true</list>
    </jql-function>
    

    与其他插件模块一样,JQL 函数模块也有一个独特的。该函数模块的另一个主要属性是函数的。在本例中,ProjectsFunction就是函数类。根元素jql-function还有两个其他元素——fnamelist

    • fname保存的是用户可见的 JQL 函数名称,这将在 JQL 查询中使用。

    • list指示函数是否返回一个列表。在我们的例子中,我们返回一个项目列表,因此我们使用true来表示它是一个列表。列表可以与操作符INNOT IN一起使用,而标量值可以与操作符=, !=, <, >, <=, >=, IS, IS NOT一起使用。

  2. 实现函数类:

    这里的类名是模块描述中使用的名称,在本例中为ProjectsFunction。该类应当扩展AbstractJqlFunction类。接下来,我们需要实现下面详细介绍的主要方法:

    • getDataType - 该方法定义了函数的返回类型。在我们的例子中,我们接收一个项目键的列表并返回有效的项目,因此我们将实现该方法以返回PROJECT数据类型,如下所示:

      public JiraDataType getDataType() {
        return JiraDataTypes.PROJECT;
      }
      

      查看JiraDataTypes类以了解其他支持的数据类型。

    • getMinimumNumberOfExpectedArguments - 它返回该函数可能接受的最小参数数量。在问题导航器中自动填充的方法会考虑到这一点,并在选择函数时在括号内自动放入足够的双引号。

      例如,在我们的例子中,我们至少需要在函数名称中包含一个项目键,因此我们return 1,如下所示:

      public int getMinimumNumberOfExpectedArguments() {
        return 1;
      }
      

      预填充的函数将类似于projects("")

    • validate – 该方法用于验证我们传递的参数。在我们的示例中,我们需要检查方法是否至少有一个参数,并确保传递的所有参数都是有效的项目密钥。validate方法如下所示:

      public MessageSet validate(User searcher, FunctionOperand operand, TerminalClauseterminalClause) {
        List<String> projectKeys = operand.getArgs();
        MessageSet messages = new MessageSetImpl();
        if (projectKeys.isEmpty()) {
          messages.addErrorMessage("Atleast one project key needed");
        } else {
          for (String projectKey : projectKeys) {
            if (projectManager.getProjectObjByKey(projectKey) == null){
              messages.addErrorMessage("Invalid Project Key:" + projectKey);
            }
          }
        }
        return messages;
      }
      

    在这里,我们实例化一个新的MessageSet并将错误信息添加到其中,如果验证失败。我们必须始终返回一个MessageSet,即使它为空。返回null是不允许的。我们还可以添加警告信息,这不会阻止 JQL 的执行,但会提醒用户某些事情。

    validate方法中最重要的参数是FunctionOperand,因为它持有函数的参数,可以通过operand.getArgs()检索。另一个参数terminalClause是 JIRA 表示我们正在验证的 JQL 条件。我们可以使用terminalClause.getNameterminalClause.getOperatorterminalClause.getOperand分别提取名称、操作符和函数。

    AbstractJqlFunction中有一个验证方法,用于检查参数的数量。所以,如果我们知道预期的参数数量(在我们的示例中并不适用,因为可以传递任意数量的项目),我们可以使用以下方法进行验证:

    MessageSet messages = validateNumberOfArgs(operand, 1);
    

    如果参数数量不为 1,则此代码会添加一个错误。

    • getValues – 这是一个方法,接受参数并根据函数返回日期类型的列表或标量。在我们的示例中,getValues方法返回一个包含项目 ID 的字面量列表。

      在我们的示例中,方法的实现如下:

      public List<QueryLiteral> getValues(QueryCreationContext context, FunctionOperand operand,   TerminalClauseterminalClause) {
        notNull("queryCreationContext", context);
        List<QueryLiteral> literals = new LinkedList<QueryLiteral>();
        List<String> projectKeys = operand.getArgs();
        for (String projectKey : projectKeys) {
          Project project = projectManager.getProjectObjByKey(projectKey);
          if (project != null) {
            literals.add(new QueryLiteral(operand, project.getId()));
          }
        }
        return literals;
      }
      

      notnull()Asserions类中的一个预定义方法,用于检查查询创建上下文是否为空,如果为空则抛出错误。这不是强制性的,如果需要,可以以其他方式处理。

      参数operandterminalClause与我们在validate方法中看到的相同。QueryCreationContext参数持有查询执行时的上下文。QueryCreationContext.getUser将检索执行查询的用户,而QueryCreationContext.isSecurityOverriden方法指示此功能是否应该执行安全检查。

      该函数应始终返回一个QueryLiteral对象的列表。即使函数返回标量而不是列表,它也应返回一个QueryLiteral列表,可以按以下方式创建:

      Collections.singletonList(new QueryLiteral(operand, some_value))
      

      QueryLiteral表示StringLongEMPTY值。这三者代表了 JQL 的可区分类型。如果没有值,它表示 EMPTY;如果使用字符串构造,它表示一个字符串;如果使用 Long 构造,它表示一个 Long。

      在我们的例子中,我们使用项目 ID(LONG),它在项目中是唯一的。对于项目,我们甚至可以使用密钥(STRING)或名称(STRING),因为它们也是唯一的。然而,这种方法可能不适用于类似 Fix For Version 这样的字段,因为你可能会发现两个 Fix Version 拥有相同的名称。建议尽可能返回 ID,以避免产生模糊的搜索结果。

      总结来说,我们通过用户提供的项目密钥查找项目对象,并返回一个使用项目 ID 创建的QueryLiterals列表。

  3. 打包插件并进行部署。

工作原理...

一旦插件部署完成,我们可以进入问题导航器并打开高级搜索,开始使用我们全新的函数!当你开始输入project in p时,JIRA 会自动填充可用的选项,包括我们的新函数,如下所示:

工作原理...

一旦添加了适当参数的函数,搜索会被执行,结果如下所示:

工作原理...

当提供无效的项目密钥作为参数时,我们的validate方法将填充错误信息,如下图所示:

工作原理...

另见

  • 在第一章中,创建骨架插件插件开发过程

  • 部署你的插件

清理 JQL 函数

如果你不想让你的 JQL 函数违反你 JIRA 实例的严格安全性,清理 JQL 函数是必须的!那么,这到底意味着什么呢?

想象一下,你创建了一个过滤器,用于查找预定义项目集合中的问题。如果你将该过滤器分享给一个不应该看到该项目或不知道项目存在的朋友,会发生什么?你分享过滤器的那个人将无法修改受保护项目中的问题,因为 JIRA 的权限方案限制了他们的操作,但他/她肯定会在过滤器中使用的 JQL 查询中看到项目的名称!

这就是 JQL 函数清理能够提供帮助的地方。本质上,我们只是修改 JQL 查询,以根据权限方案保护参数。让我们来看一个通过清理我们在前一个例子中创建的 JQL 函数来做到这一点的示例。

准备工作

开发 JQL 函数,如前一个例子中所述。

如何操作...

在我们的 JQL 函数中,我们使用项目密钥作为参数。为了说明函数清理过程,我们将替换掉用户没有权限浏览项目时的密钥,而改用项目 ID。以下是逐步展示如何实现这一操作的过程:

  1. 修改 JQL 函数类以实现 ClauseSanitisingJqlFunction 接口:

    public class ProjectsFunction extends AbstractJqlFunction implements ClauseSanitisingJqlFunction{
    
  2. 实现 sanitiseOperand 方法:

    @NotNull FunctionOperand santiseOperand(User searcher, @NotNullFunctionOperand operand);
    

    在这里,我们从FunctionOperand参数中读取所有现有的 JQL 函数参数,并修改它,使其在用户没有浏览权限的地方使用项目 ID 替代项目密钥:

    public FunctionOperand sanitiseOperand(User user, FunctionOperand functionOperand) {
      final List<String> pKeys = functionOperand.getArgs();
      boolean argChanged = false;
      final List<String> newArgs = new ArrayList<String>(pKeys.size());
      for (final String pKey : pKeys) {
        Project project = projectManager.getProjectObjByKey(pKey);
        if (project != null && !permissionManager.hasPermission(Permissions.BROWSE, project, user)) {
          newArgs.add(project.getId().toString());
          argChanged = true;
        } else {
          newArgs.add(pKey);
        }
      }
    
      if (argChanged) {
        return new FunctionOperand(functionOperand.getName(),
    newArgs);
      } else {
        return functionOperand;
      }
    }
    
  3. 打包并部署修改后的插件。

它是如何工作的……

插件部署后,如果用户没有浏览项目的权限,他/她将看到项目 ID,而不是原本在创建过滤器时输入的密钥。以下是此情况下查询如何显示的示例截图。在这种情况下,我只是将自己从TEST项目的浏览权限中移除,您可以看到查询已修改,将密钥TEST替换为其唯一 ID,从而不会泄露太多信息!

它是如何工作的...

如果您现在尝试编辑过滤器会怎样?我们的验证机制将启动,因为它无法找到带有该 ID 的项目,如下所示!不错吧?

它是如何工作的...

这只是一个示例,我们可以以类似的方式清理其他所有情况中的查询。

另见

  • 编写 JQL 函数

添加搜索请求视图

JIRA 中的一个可定制功能是问题导航器。它让我们可以根据多种标准进行搜索,选择需要显示的字段,并以我们想要的方式展示!

问题导航器中的正常视图或默认视图是表格视图,用于显示我们通过配置问题导航器所选择的问题和字段。JIRA 还提供了几种其他选项,可以以不同的格式查看搜索结果,导出为 Excel、Word 或 XML 等,所有这些都可以通过预定义的搜索请求视图实现。

在本教程中,我们将学习如何向 JIRA 添加更多搜索视图,使我们可以以自己喜欢的格式查看搜索结果。为实现这一目标,我们需要使用搜索请求视图插件模块。

准备工作

使用 Atlassian Plugin SDK 创建插件框架。

如何操作……

如前所述,我们使用搜索请求视图插件模块来创建自定义搜索视图。在我们的示例中,我们将创建一个简单的 HTML 视图,只显示问题的关键字和摘要。

以下是逐步过程:

  1. 使用搜索请求视图模块定义插件描述符:

    <search-request-view key="simple-searchrequest-html" name="Simple HTML View" class="com.jtricks.SimpleSearchRequestHTMLView" state='enabled'                        fileExtension="html" contentType="text/html">
      <resource type="velocity" name="header" location="templates/searchrequest-html-header.vm"/>
      <resource type="velocity" name="body" location="templates/searchrequest-html-body.vm"/>
      <resource type="velocity" name="footer" location="templates/searchrequest-html-footer.vm"/>
    
      <order>200</order>
    </search-request-view>
    

    像往常一样,模块有一个唯一的键。以下是其他属性:

    • name:将在问题导航器中显示的视图名称

    • class:搜索请求视图类。我们在这里填充 Velocity 上下文,提供必要的信息。

    • contentType:生成文件的 contentTypetext/htmltext/xmlapplication/rss+xmlapplication/vnd.ms-wordapplication/vnd.ms-excel

    • fileExtension:生成文件的扩展名。htmlxmldocxls

    • state:启用或禁用。决定模块是否在启动时启用

    Search-Request-View 元素还包含一些子元素,用于定义不同视图所需的 Velocity 模板,并确定视图出现的orderorder值较低的模块将首先显示。JIRA 对内置视图使用的是 10 的order值。较小的值会将新视图显示在内置视图之上,而较大的值则会将新视图放置在底部。

  2. 实现搜索请求视图类。

    搜索请求视图类必须实现SearchRequestView接口。为了简化操作,我们可以扩展已经实现该接口的AbstractSearchRequestView类。这样,我们只需要实现一个方法——writeSearchResults

    该方法接受一个 writer 参数,借此我们可以使用定义的各种模板视图生成输出。例如:

    writer.write(descriptor.getHtml("header", headerParams));
    

    它会识别名为header的 Velocity 模板,并使用映射中的headerParams变量来渲染模板。我们可以类似地定义任意数量的模板,并将它们写入以创建所需的视图。

    在我们的示例中,定义了三个视图——头部、主体和尾部。这些视图可以根据我们需要命名,但我们在atlassian-plugin.xml中定义的相同名称应在搜索请求视图类中使用。

    在我们的类实现中,我们使用三个视图来生成简单的 HTML 视图。我们在开始和结束时使用头部和尾部视图,并使用主体视图为搜索结果中的每个单独问题生成视图。以下是我们如何操作的:

    • 生成一个包含默认 Velocity 上下文参数的映射:

      final Map defaultParams = JiraVelocityUtils.getDefaultVelocityParams(authenticationContext);
      
    • 用我们需要的变量填充映射,以便渲染头部模板并写出头部。在我们的示例中,我们将头部保持得相当简单,仅使用过滤器名称和当前用户:

      final Map headerParams = new HashMap(defaultParams);        headerParams.put("filtername", searchRequest.getName());        headerParams.put("user", authenticationContext.getUser());
      writer.write(descriptor.getHtml("header", headerParams));
      
    • 现在我们需要写出搜索结果。我们应该遍历每个搜索结果中的问题,并使用我们定义的格式将其写入到 writer 中。为了避免导致巨大的内存消耗,每次只加载一个问题到内存中。这可以通过使用HitCollector来保证。该 Collector 负责在遇到每个搜索结果时将其写出。Lucene 搜索代码会在每个搜索结果时调用它:

      final Searcher searcher = searchProviderFactory.getSearcher(SearchProviderFactory.ISSUE_INDEX);
      final Map issueParams = new HashMap(defaultParams);
      final DocumentHitCollectorhitCollector = new IssueWriterHitCollector(searcher, writer, issueFactory){
        protected void writeIssue(Issue issue, Writer writer) throws IOException{
          //put the current issue into the velocity context and render the //single issue view
          issueParams.put("issue", issue  writer.write(descriptor.getHtml("body", issueParams));
        }
      };
      searchProvider.searchAndSort(searchRequest.getQuery(), user, hitCollector, searchRequestParams.getPagerFilter());
      

    在这里,我们所做的只是定义HitCollector并调用searchAndSort方法,后者将使用HitCollector为每个问题生成视图。如果需要的话,我们可以在此添加更多变量。

    • 我们现在可以在结束前写出尾部。为了教育目的,我们再次插入用户信息:

      writer.write(descriptor.getHtml("footer", EasyMap.build("user", user)));
      

    在这里,我们创建了一个简单的映射,只是为了展示我们只需要在视图中使用的变量。

    该方法现在看起来如下:

    @Override
    public void writeSearchResults(final SearchRequestsearchRequest, final SearchRequestParams searchRequestParams, final Writer writer) throws SearchException{
      final Map defaultParams = JiraVelocityUtils.getDefaultVelocityParams(authenticationContext);
      final Map headerParams = newHashMap(defaultParams);          headerParams.put("filtername", searchRequest.getName());      headerParams.put("user", authenticationContext.getUser());  
      try{
        //Header
        writer.write(descriptor.getHtml("header", headerParams));  
    
        //Body
        final Searcher searcher =searchProviderFactory.getSearcher(SearchProviderFactory.ISSUE_INDEX);
        final Map issueParams = new HashMap(defaultParams); 
        final DocumentHitCollector hitCollector = new IssueWriterHitCollector(searcher, writer, issueFactory) {
          protected void writeIssue(Issue issue, Writer writer) throws IOException{
            //put the current issue into the velocity context and render the single issue view
            issueParams.put("issue", issue);      writer.write(descriptor.getHtml("body", issueParams));
          }
        };
        searchProvider.searchAndSort(searchRequest.getQuery(), authenticationContext.getUser(),hitCollector, searchRequestParams.getPagerFilter());
    
        //Footer
        writer.write(descriptor.getHtml("footer", EasyMap.build("user", authenticationContext.getUser())));
      }catch (IOException e){
        throw new RuntimeException(e);
      }catch (SearchException e){
        throw new RuntimeException(e);
      }
    }
    
  3. 编写 Velocity 模板。如我们所见,我们使用了三个视图:

    • 头部 – 速度模板是 templates/searchrequest-html-header.vm。以下代码展示了它的样子:

      Hello $user.fullName , have a look at the search results!<br><br>
      #set($displayName = 'Anonymous')
      #if($filtername)
        #set($displayName = $textutils.htmlEncode($filtername))
      #end
      <b>Filter</b> : $displayName<br><br>
      <table>
      

    我们只需向用户问候并显示筛选器名称。它还包含一个 <table> 标签,用于开始问题表格。表格将在页脚关闭。

    • 主体 – 速度模板是 templates/searchrequest-html-body.vm。以下代码展示了它的样子:

      <tr>
        <td><font color="green">$!issue.key</font></td>
        <td>$!issue.summary</td>
      </tr>
      

      无论这里出现什么内容,都是所有问题的共同部分。这里我们为每个问题创建一行,并适当地显示键值和摘要。

    • 页脚 – 速度模板是 templates/searchrequest-html-footer.vm。以下代码展示了它的样子:

      </table>
      <br><br>...And that's all we have got now , $user.fullName !
      

      我们只需关闭表格并用一条消息结束!

  4. 打包插件并部署它。

它是如何工作的...

插件部署后,我们将在问题导航器中找到一个名为 简单 HTML 视图 的新视图:

它是如何工作的...

选择视图后,当前搜索结果将如下所示:

它是如何工作的...

如果结果属于某个筛选器,它将显示筛选器名称,而不是显示匿名:

它是如何工作的...

现在就看我们的创意了,如何让它更漂亮,或者使用完全不同的内容类型代替 HTML。有关如何生成 XML 视图的示例,请参见 JIRA 文档中的 developer.atlassian.com/display/JIRADEV/Search+Request+View+Plugin+Module

参见

  • 创建一个骨架插件 在 第一章,插件开发过程

  • 部署你的插件

使用快速搜索进行智能查询

这个名字说明了一切!JIRA 通过其快速搜索功能支持智能查询,使用户能够轻松找到关键信息。JIRA 识别一组预定义的搜索关键字,我们可以使用它们快速、智能地进行搜索!

在这个示例中,我们将学习如何在一些 JIRA 字段上进行智能查询。

如何操作...

在开始之前,快速搜索 框位于 JIRA 右上角,如下所示:

如何操作...

以下是 JIRA 4.4 中如何对某些字段进行搜索的方法。别忘了检查你的 JIRA 版本支持哪些字段!

  • 问题键:如果你已经知道想要查看的问题键,那就更方便了!只需在 快速搜索 框中输入问题键,JIRA 就会带你到该问题页面。

    还有更多!如果你正在浏览一个项目或查看一个问题,并且想查看另一个已知键的问题,你只需要输入唯一键的数字部分(仅输入数字)。甚至不需要输入完整的键值。

    例如,TEST-123 会直接带你到该问题。输入 125 会将你带到 TEST-125

  • 项目:如果输入项目的关键字,快速搜索将显示该特定项目中的所有问题。只要项目名称中没有空格,也可以使用项目名称。

    例如,TEST 将返回所有在项目 TEST 中或关键字为TEST的项目中的问题。而“TEST Project”不会显示名称为“Test Project”的项目中的问题,因为快速搜索将其解释为两个不同的关键字。

  • 被分配人:可以使用关键字my来查找所有分配给我的问题。

  • 报告人:关键字r:后跟me,或者报告人名称,可以查找由我或指定用户报告的所有问题。例如,r:none 也支持,它会返回没有报告人的问题。

    r:me 将检索所有由我报告的问题,而r:admin 将检索所有由用户 admin 报告的问题。

  • 日期 字段:可以基于问题中的三个主要日期字段进行快速搜索——创建时间更新时间到期时间。使用的关键字分别是createdupdateddue。关键字后面应跟着:和没有空格的日期范围。

    日期范围可以使用以下关键字之一——today(今天)、tomorrow(明天)、yesterday(昨天),或者是单一日期范围(例如,‘-5d’)或两个日期范围(例如,‘-2w,1w’)。日期范围之间不能有空格。有效的日期/时间缩写为:‘w’(周)、‘d’(天)、‘h’(小时)和‘m’(分钟)。例如:

    • created:today 将检索所有今天创建的问题。

    • updated:-5d 将检索所有在过去五天内更新的问题。

    • due:-2w,1w 将检索所有在过去两周内到期且在接下来一周内到期的问题。

    你还可以使用关键字overdue来检索所有已过期(有过去的到期日期)的问题。

  • 优先级:可以使用以下优先级值进行快速搜索:blocker(阻塞)、critical(关键)、major(重大)、minor(次要)和trivial(微不足道)。只需键入相应的值即可检索所有具有给定优先级值的问题。

    例如,通过使用major可以检索所有优先级为major的问题。

  • 问题类型:只要问题类型名称中没有空格,就可以在快速搜索中使用。即使是复数形式也能有效。

    例如,输入bugbugs将检索所有类型为 bug 的问题。

  • 版本:快速搜索可以使用关键字v:ff:后跟版本值,查找具有已知受影响版本或修复版本的问题,且版本名之间不能有空格。也可以使用通配符进行搜索。搜索还将找到所有包含你指定字符串的版本值,字符串后面紧接着一个空格。例如:

    • v:2.0 将查找版本为 2.0、2.0 one、2.0 beta 等的所有问题。但它不会查找版本为 2.0.1 的问题。

    • v:2.* 将查找版本为 2.0、2.0 one、2.0.1、2.2 等的问题。

    对于版本的修复,情况也是一样的。前缀仅更改为 ff:

  • 组件:快速搜索可以使用前缀 c: 加上组件名称来查找带有组件名称的所有问题。它会检索所有组件名称中包含该值的所有问题,而不一定是以该值开头的。

    例如,c:jql将查找所有包含 'jql' 字样的组件中的问题。它适用于组件 jql、jql 性能、先进的 jql 等。

还有更多...

快速搜索也可以用来搜索问题中任何包含的单词,只要该单词出现在问题的摘要、描述或评论中。它被称为 智能 搜索

如果你认为你想在不使用智能搜索的情况下使用这些关键词,则可以在显示结果时不使用智能搜索来运行查询。

智能查询可以结合多个关键词来缩小搜索范围。它甚至可以与自由文本搜索结合使用。

例如,我未解决的 bug将检索所有已打开并分配给我的 bug。它等同于以下 JQL:

issuetype = Bug AND assignee = currentUser() AND status = Open

我的未解决 bug jql将检索所有已打开并分配给我的 bug,并且在其摘要、描述或评论中包含 'jql' 这个词。它等同于:

(summary ~ jql OR description ~ jql OR comment ~ jql) AND issuetype = Bug AND assignee = currentUser() AND status = Open

我的未解决 bug jql 性能等同于:

(summary ~ "jql performance" OR description ~ "jql performance" OR comment ~ "jql performance") AND issuetype = Bug AND assignee = currentUser() AND status = Open.

更多关于高级搜索或 JQL 的内容,请参见 confluence.atlassian.com/display/JIRA/Advanced+Searching

插件中的搜索

随着 JQL 的发明,JIRA 搜索 API 与 3.x 版本相比发生了巨大变化。插件中的搜索现在通过支持 JQL 的 API 完成。在本示例中,我们将看到如何使用这些 API 在插件中搜索问题。

如何操作...

为了专注于搜索 API,我们将编写一个简单的方法 getIssues(),根据一些搜索条件返回问题对象的列表。

搜索的本质是使用 JqlQueryBuilder 构建一个 Query 对象。Query 对象将具有 where 子句和 order by 子句,这些子句是通过 JqlClauseBuilder 构建的。我们还可以在子句之间使用 ConditionBuilders 加入条件。

现在,假设我们想在我们的插件中查找所有属于特定项目(项目 ID:10000,项目 Key:DEMO)并分配给当前用户的问题。其 JQL 等价语句为:

project = "DEMO" and assignee = currentUser()

以下是执行此操作的步骤:

  1. 创建一个 JqlQueryBuilder (docs.atlassian.com/software/jira/docs/api/latest/com/atlassian/jira/jql/builder/JqlQueryBuilder.html) 对象。

    JqlQueryBuilder 用于构建用于执行问题搜索的查询。以下是如何创建 JqlQueryObject 的示例:

    JqlQueryBuilder builder = JqlQueryBuilder.newBuilder();
    
  2. 创建一个返回JqlClauseBuilderwhere子句(docs.atlassian.com/software/jira/docs/api/latest/com/atlassian/jira/jql/builder/JqlClauseBuilder.html)。查询是通过一个或多个 JQL 子句构建的,每个子句之间可以添加不同的条件。

    builder.where()返回一个JqlClauseBuilder对象,用于我们的QueryBuilder,在这个对象上我们可以添加多个子句。

  3. 添加项目子句来搜索指定 ID 的项目。项目子句将返回一个ConditionBuilder

    builder.where().project(10000L)
    
  4. 使用AND条件在ConditionBuilder中添加assignee子句:

    builder.where().project(10000L).and().assigneeIsCurrentUser();
    

    我们可以像这样使用不同的条件添加多个子句。让我们在“还有更多...”部分看到一些示例。

  5. 如果有排序需求,可以使用Order By子句来添加排序。我们可以按指派人排序,如下所示:

    builder.orderBy().assignee(SortOrder.ASC);
    

    SortOrder.DESC可以用于降序排序。

  6. 构建Querycom.atlassian.query.Query)对象:

    Query query = builder.buildQuery();
    

    Query对象是不可变的;一旦创建,它就不能更改。JqlQueryBuilder表示Query对象的可变版本。我们可以通过调用JqlQueryBuilder.newBuilder(existingQuery)从一个已存在的查询创建一个新的查询。

  7. 获取SearchService的实例。它可以通过依赖注入在插件的构造函数中注入,也可以通过ComponentManager类按如下方式获取:

    SearchService searchService = ComponentManager.getInstance().getSearchService();
    
  8. 使用查询搜索以获取搜索结果(docs.atlassian.com/jira/latest/com/atlassian/jira/issue/search/SearchResults.html):

    SearchResults results = searchService.search(user, query, PagerFilter.getUnlimitedFilter());
    

    这里我们使用了PagerFilter.getUnlimitedFilter()来获取所有结果。也可以通过方法PagerFilter.newPageAlignedFilter(index, max)限制结果到特定范围,比如从 20 到 80 个结果。这在分页时非常有用,例如在问题导航器的情况下。

  9. 从搜索结果中获取问题:

    List<Issue> issues = results.getIssues();
    

整个方法将如下所示:

  private List<Issue>getIssues(User user) {
    JqlQueryBuilder builder = JqlQueryBuilder.newBuilder();
    builder.where().project(10000L).and().assigneeIsCurrentUser();
    builder.orderBy().assignee(SortOrder.ASC);
    Query query = builder.buildQuery();
    SearchService searchService = ComponentManager.getInstance().getSearchService();
    SearchResults results = searchService.search(user, query, PagerFilter.getUnlimitedFilter());
    returnresults.getIssues();
  }

希望这能为编写更复杂的查询提供一个良好的起点!

还有更多...

正如之前承诺的那样,让我们通过几个示例来看一下如何编写复杂的查询。

  • 我们可以扩展前述的搜索,包含多个项目、指派人和自定义字段。该查询的 JQL 表示将是:

    project in ("TEST", "DEMO") and assignee in ("jobinkk", "admin") and "Customer Name" = "Jobin"
    

    where子句写作:

    builder.where().project("TEST", "DEMO").and().assignee().in("jobinkk", "admin").and().customField(10000L).eq("Jobin");
    

    10000L 是自定义字段“客户名称”的 ID。

  • 我们可以使用sub()endsub()来分组条件,从而编写更加复杂的查询:

    project in ("TEST", "DEMO") and (assignee is EMPTY or reporter is EMPTY)
    

    它可以写成:

    builder.where().project("TEST", "DEMO").and().sub().assigneeIsEmpty().or().reporterIsEmpty().endsub();
    

同样,我们可以编写更复杂的查询。

另请参见

  • 编写 JQL 函数

在插件中解析 JQL 查询

在前面的例子中,我们看到了如何在 JIRA 中构建查询进行搜索。在本例中,我们将再次进行搜索,但不使用 API 构建查询。我们将直接使用在问题导航器的高级模式中书写的 JQL 查询进行搜索。

如何实现它...

假设我们知道要执行的查询。假设它与我们在前面的例子中看到的是相同的:project = "DEMO" and assignee = currentUser()

以下是我们如何做到的:

  1. 解析 JQL 查询:

    String jqlQuery = "project = \"DEMO\" and assignee = currentUser()";
    SearchService.ParseResult parseResult = searchService.parseQuery(user, jqlQuery);
    
  2. 检查解析结果是否有效:

    if (parseResult.isValid()){
       // Carry On
    } else {
      // Log the error and exit!
    }
    
  3. 如果结果有效,从ParseResult中获取Query对象:

    Query query = parseResult.getQuery()
    
  4. 搜索问题并获取SearchResults,就像我们在前面的例子中所看到的那样:

    SearchResults results = searchService.search(user, query, PagerFilter.getUnlimitedFilter());
    
  5. 从搜索结果中检索问题列表:

    List<Issue> issues = results.getIssues();
    

它是如何工作的...

在这里,SearchService中的parseQuery操作将String类型的 JQL 查询转换为我们通常使用JqlQueryBuilder构建的Query对象。实际的解析操作由JqlQueryParser在幕后完成。

另见

  • 插件中的搜索

直接链接到搜索查询

你有没有想过如何从模板或 JSP 页面通过自定义页面或插件页面链接到查询?在本例中,我们将看到如何以编程方式以及其他方式创建链接,并在不同的地方使用它。

如何实现它...

让我们首先看看如何以编程方式创建搜索链接。执行以下步骤:

  1. 使用JqlQueryBuilder创建Query对象,就像我们在前面的例子中所看到的那样。

  2. 获取SearchService的实例。它可以通过依赖注入注入到插件的构造函数中,也可以从ComponentManager类中按以下方式检索:

    SearchService searchService = ComponentManager.getInstance().getSearchService();
    
  3. 使用SearchServiceQuery对象中检索查询字符串,如下所示:

    String queryString = searchService.getQueryString(user, query);
    
  4. 使用上下文路径构造链接。在 JSP 中,你可以按以下方式进行:

    <a href="<%= request.getContextPath() %>/secure/IssueNavigator.jspa?reset=true<ww:property value="/queryString" />&amp;mode=hide" title="">Show in Navigator</a>
    

    这里,Action类中的getQueryString()返回前面的queryString

    在 Velocity 模板中:

    <a href="$requestContext.baseUrl/secure/IssueNavigator.jspa?reset=true$queryString&amp;mode=hide" title="">Show in Navigator</a>
    

    这里,$queryString是上下文中的前一个queryString

    mode参数可以有hideshow两种值,取决于你是希望在查看模式还是编辑模式下打开问题导航器!

它是如何工作的...

SearchService中的getQueryString方法以可以用于 URL 的方式返回queryString。它以&jqlQuery=开头,后面跟随实际的查询,以 Web URL 的形式表示:

reset=true<ww:property value="/queryString" />&amp;mode=hide will be then reset=true&amp;jqlQuery=someQuery&amp;mode=hide

还有更多...

链接到快速搜索也非常简单且有用。我们甚至可以将这样的搜索存储在浏览器的收藏夹中。我们需要做的就是通过以下方式替换 JIRA URL 中的%s,来找出 URL:

http://<Context_Path>/secure/QuickSearch.jspa?searchString=%s

例如,如果你的 JIRA 实例是http://localhost:8080/,并且你想快速搜索所有由你担任负责人(assignee)的任务,则相关的快速搜索字符串将是:my open

然后,URL 将是:

http://localhost:8080/secure/QuickSearch.jspa?searchString=my+open

请注意,Quick Search 中的空格在替换%s时会被替换为+

其他示例:

  • http://localhost:8080/secure/QuickSearch.jspa?searchString=my+open+critical 检索分配给您的所有未解决的紧急问题。

  • http://localhost:8080/secure/QuickSearch.jspa?searchString=created:-1w+my 检索过去一周内分配给您的所有问题。

程序化索引和取消索引

正如我们在 JIRA 架构中解释的那样,理解插件框架,JIRA 中的搜索基于 Apache Lucene。Lucene 索引存储在文件系统中,并作为在 JIRA 中执行的搜索查询的基础。每当问题被更新时,会在文件系统中为该特定问题创建更多记录或更新现有记录。

可以程序化地为选定的或所有问题建立索引或取消索引问题。此外,如果需要,我们还可以在插件中选择性地关闭或开启索引。在这个示例中,我们将看到这两种操作。

如何执行...

大多数索引操作可以借助IssueIndexManager来完成。可以通过构造函数注入或如下方式创建IssueIndexManager的实例:

IssueIndexManager indexManager = ComponentManager.getInstance().getIndexManager();

以下是IssueIndexManager支持的重要操作:

  • reIndexAll() – 为 JIRA 中的所有问题建立索引。如果您希望自定义管理员操作执行索引,这是一个不错的方法!

  • reIndex(GenericValue issue)reIndex(Issue issue) – 通过传递Issue对象或其GenericValue来选择性地为一个问题建立索引。

  • deIndex(GenericValue issue) – 用于取消索引某个问题的方法。一旦完成,该问题将不会出现在搜索结果中。

    请注意,当问题稍后更新或在问题上添加评论时,JIRA 会自动重新建立索引。因此,不要依赖于仅调用一次来永久地将问题从搜索中隐藏。为此,IssueIndexer应被覆盖,以避免重新为该问题建立索引。

  • reIndexIssues(final Collection<GenericValue> issues)reIndexIssueObjects(final Collection<? extends Issue>issueObjects) – 为一组问题建立索引。

请查看 Java 文档中的IssueIndexManager了解更多IssueIndexManager的可用方法。

如果我们想确保在对问题进行重大更新时索引被启用,我们可以做如下操作:

  // Store the current state of indexingboolean wasIndexing = ImportUtils.isIndexIssues();
  // Set indexing to trueImportUtils.setIndexIssues(true);
  // Update the issue or issues
  ...................
  // Reset indexingImportUtils.setIndexIssues(wasIndexing);

在这里,我们使用ImportUtils来保存当前的索引状态并将其开启。更新完问题后,索引将恢复到原始状态!

另请参见

  • 插件中的搜索

程序化管理过滤器

无论是 JIRA 新手还是专家,常用的功能之一是创建和管理过滤器。我们可以保存搜索、共享它们并订阅它们,这为 JIRA 增添了很大的价值。那么,我们如何通过编程方式创建和管理过滤器呢?

在本实例中,我们将学习如何通过编程方式管理过滤器。

如何执行...

我们将一一查看管理过滤器的各个方面:

创建过滤器

管理过滤器的大多数操作是通过SearchRequestService完成的。创建过滤器的步骤如下:

  1. 创建要保存为过滤器的查询。可以使用JqlQueryBuilder创建查询,正如我们在之前的实例中看到的那样。

  2. 从查询创建一个SearchRequest对象

    SearchRequest searchRequest = new SearchRequest(query);
    
  3. 创建 JIRA 服务上下文。如果在一个 action 类中,可以通过调用getJiraServiceContext()来获取服务上下文,否则,可以像这样创建一个实例:

    JiraServiceContext ctx = new JiraServiceContextImpl(user);
    

    其中,user是过滤器应为其创建的用户。

  4. 获取SearchRequestService的实例。可以通过构造函数注入,也可以如下获取:

    SearchRequestService searchRequestService = ComponentManager.getInstance().getSearchRequestService();
    
  5. 创建过滤器:

    final SearchRequest newSearchRequest = searchRequestService.createFilter(ctx, searchRequest, favourite);
    

    其中,favourite是一个布尔值,如果要将过滤器设为收藏,可以设置为true

更新过滤器

更新过滤器与创建过滤器非常相似。一旦更新了SearchRequest并创建了上下文,我们需要调用以下方法,以更新并将新的查询参数(即新查询)保存到数据库中:

SearchRequest updatedSearchRequest = searchRequestService.updateSearchParameters(JiraServiceContextserviceCtx, SearchRequest request);

要更新诸如名称、描述等属性,调用以下方法之一,具体取决于我们是否希望将过滤器设为收藏:

SearchRequest updatedFilter = searchRequestService.updateFilter(JiraServiceContextserviceCtx, SearchRequest request);

或者,我们可以使用:

SearchRequest updatedFilter = searchRequestService.updateFilter(JiraServiceContextserviceCtx, SearchRequest request, booleanisFavourite);

删除过滤器

JIRA 以过滤器 ID 作为输入来删除过滤器。在实际删除过滤器之前,我们需要先进行删除验证,操作如下:

searchRequestService.validateForDelete(ctx, filterId);

如果出现任何错误,它将被添加到 Action 的错误集合中。然后,我们可以检查错误并在没有错误的情况下删除过滤器。

if(!ctx.getErrorCollection().hasAnyErrors())){
  searchRequestService.deleteFilter(ctx, filterId);
}

我们也可以使用以下方法删除用户的所有过滤器:

deleteAllFiltersForUser(JiraServiceContextserviceCtx, User user);

检索过滤器

SearchRequestService也有一些方法可以检索收藏的过滤器、用户拥有的过滤器、非私有过滤器等。关键方法如下所示:

Collection<SearchRequest>getFavouriteFilters(User user);
Collection<SearchRequest>getOwnedFilters(User user);
Collection<SearchRequest>getNonPrivateFilters(User user);
Collection<SearchRequest>getFiltersFavouritedByOthers(User user);

方法名称不言自明。

共享过滤器

为了共享一个过滤器,我们需要检索相关的过滤器并使用以下方法设置权限:

searchRequest.setPermissions(permissions);

其中,permissions是一个SharePermission对象的集合。可以通过SharePermissionUtils工具类从 JSONArray 创建SharePermission对象。JSONObject 可以有三个键——TypeParam1Param2

Type可以有以下值:globalgroupproject

  • Typeglobal时,Param1Param2不是必需的。

  • 当它是group时,Param1将被填充为groupname

  • 当它是project时,Param1是项目的 ID,Param2是项目角色的 ID。

JSON 数组的示例如下:

[{"type":"global"}]
[{"type":"group","param1":"jira-administrators"},{"type":"project","param1":"10000","param2":"10010"}]

另见

  • 插件中的搜索

订阅一个过滤器

我们已经看到了管理过滤器的各种方法。虽然过滤器是保存搜索并在以后快速访问的好方法,但过滤器订阅更好!订阅帮助我们定期查看感兴趣的问题,而无需登录 JIRA。

我们如何在程序中订阅过滤器?在本教程中,我们将专注于在插件中订阅过滤器。

如何操作...

对于过滤器的订阅,JIRA 提供了一个管理类,实现了 FilterSubscriptionService 接口。这个类提供了管理过滤器订阅所需的重要方法。

过滤器订阅有三个重要参数:

  1. Cron 表达式:这是订阅中最重要的部分。它告诉我们订阅何时运行,换句话说,它定义了订阅的时间表。

    Cron 表达式由以下字段组成,字段之间用空格分隔。

    字段 允许的值 允许的特殊字符
    Second 0-59 , - * /
    Minute 0-59 , - * /
    Hour 0-23 , - * /
    Day-of-Month 1-31 , - * / ? L W C
    Month 1-12 或 JAN-DEC , - * /
    Day-of-week 1-7 或 SUN-SAT , - * / ? L C #
    Year (Optional) 1970-2099 , - * /

    特殊字符表示以下内容:

    特殊字符 用法
    , 值的列表。例如,'MON,WED,FRI' 表示“每个星期一、星期三和星期五”。
    - 值的范围。例如,'MON-WED' 表示“每个星期一、星期二和星期三”。
    * 所有可能的值。例如,Hour 字段中的 * 表示“每天的每个小时”。
    / 增量值。例如,Hour 字段中的 1/3 表示“从凌晨 1 点开始,每 3 小时一次”。
    ? 无特定值。当你只需要为 Day-of-monthDay-of-week 中的一个字段指定值,而不是另一个字段时,这个符号非常有用。

    | L | 最后一个可能的值。它的含义依据上下文不同而有所不同。例如:

    • Day-of-week 中的 L 表示“每周的最后一天”

    • 7L 表示“本月的最后一个星期六”

    • Day-of-month 中的 L 表示“本月的最后一天”

    • LW 表示“本月的最后一个工作日”

    |

    W 离给定日期最近的工作日(MON-FRI)。例如,1W 表示“离每月 1 日最近的工作日”——这在你想要得到每月的第一个工作日时非常有用!它不能与日期范围一起使用。
    # 指定某一星期几的第 N 次出现。例如,MON#3 表示“本月的第 3 个星期一”

    我们需要根据希望设置的订阅创建一个有效的 Cron 表达式。以下是基于这些规则的一些示例:

    • 0 7 30 * * ? – 每天早上 7:30

    • 0 0/15 15 * * ? – 从下午 3:00 开始,每 15 分钟一次,直到下午 3:59 结束

    你可以在 Atlassian 文档中找到更多关于过滤器订阅的示例,链接为 confluence.atlassian.com/display/JIRA/Receiving+Search+Results+via+Email

  2. 组名称:这是我们希望订阅过滤器的组。如果值为 null,它将被视为个人订阅,且上下文中的用户将被使用。

  3. 空值时发送邮件:这是一个布尔值,如果你希望即使没有结果时也发送邮件,设置为 true

现在,让我们来看一下订阅已知过滤器的步骤:

  1. 获取 FilterSubscriptionService 实例。你可以通过构造函数注入该类,或者通过 ComponentManger 类按如下方式获取:

    FilterSubscriptionService filterSubscriptionService = ComponentManager.getInstance().getComponentInstanceOfType(FilterSubscriptionService.class)
    
  2. 根据上述规则定义 cron 表达式:

    String cronExpression = "0 0/15 * * * ? *"; // Denotes every 15 minutes
    
  3. 定义组名称。如果是个人订阅,请使用 null

    String groupName = "jira-administrators";
    
  4. 创建一个 JIRA 服务上下文。如果你在一个动作类中,可以通过调用 getJiraServiceContext() 来获取服务上下文。如果不在动作类中,可以按如下方式创建实例:

    JiraServiceContext ctx = new JiraServiceContextImpl(user);
    

    其中 user 是订阅过滤器的用户,若为个人订阅,则使用上下文中的用户。

  5. 定义是否应在结果数量为零时发送电子邮件:

    booleane mailOnEmpty = true;
    
  6. 验证 cron 表达式:

    filterSubscriptionService.validateCronExpression(ctx, cronExpression);
    

    如果发生错误,JiraServiceContext 中的错误集合将会填充错误信息。

  7. 如果没有错误,使用 FilterSubscriptionService 类来存储订阅:

    if (!ctx.getErrorCollection().hasAnyErrors()){
      filterSubscriptionService.storeSubscription(ctx, filterId, groupName, cronExpression, emailOnEmpty);
    }
    

    这里的 filterId 是我们要订阅的过滤器 ID,可以通过 searchRequest.getId() 获取!

订阅现在应该已保存,邮件将根据 cron 表达式定义的计划发送。

我们还可以使用 FilterSubscriptionService 更新现有的订阅,方法如下:

filterSubscriptionService.updateSubscription(ctx, subId, groupName, cronExpression, emailOnEmpty);

其中 subId 是现有订阅的 ID!

它是如何工作的...

我们创建的每个订阅都会作为 Quartz 定时任务存储在系统中,根据我们在存储订阅时定义的 cron 表达式运行。

还有更多...

如果你想使用类似 JIRA 中的 Web 表单来创建过滤器订阅,而不想手动编写 cron 表达式,可以使用 Web 表单中的参数创建 CronEditorBean

表单中支持的各种属性可以从 CronEditorBean 类中找到。Java 文档可以在 docs.atlassian.com/software/jira/docs/api/latest/com/atlassian/jira/web/component/cron/CronEditorBean.html 查找。

一旦 CronEditorBean 被创建,它可以按如下方式解析为 cron 表达式:

String cronExpression = new CronExpressionGenerator().getCronExpressionFromInput(cronEditorBean);

另见

  • 插件中的搜索

第七章:编程问题

在本章中,我们将涵盖:

  • 从插件创建问题

  • 在问题上创建子任务

  • 更新问题

  • 删除问题

  • 添加新的问题操作

  • 问题操作的条件

  • 处理附件

  • 时间跟踪和工作日志管理

  • 处理问题上的评论

  • 编程变更日志

  • 编程问题链接

  • 问题链接验证

  • 克隆时丢弃字段!

  • 在问题字段上的 JavaScript 技巧

介绍

到目前为止,我们已经了解了如何开发自定义字段、工作流、报告和小工具、JQL 函数以及与之相关的其他可插拔组件。在本章中,我们将学习如何编程“问题”,即创建、编辑或删除问题,创建新的问题操作,以及通过 JIRA API 等管理与问题相关的各种操作。

从插件创建问题

在本教程中,我们将学习如何通过编程方式从插件创建问题。在 4.1 版本之前,JIRA 使用IssueManager来创建问题。从 JIRA 4.1 开始,推出了IssueService类来驱动问题操作。由于IssueService被推荐代替IssueManager,我们将在本教程中使用它来创建问题。

如何执行...

IssueService相较于IssueManager类的主要优点在于它处理了验证和错误处理。以下是使用IssueService创建问题的步骤:

  1. 创建IssueService类的实例。您可以在构造函数中注入它,或像下面所示通过ComponentManager获取它:

    IssueService issueService = ComponentManager.getInstance().getIssueService();
    
  2. 创建问题输入参数。在这一步中,我们将设置所有创建问题所需的值,这些值将通过IssueInputParameters类来定义。

    1. 创建IssueInputParameters类的实例。

      IssueInputParameters issueInputParameters = new IssueInputParametersImpl();
      
    2. 使用下几行代码,将所需的值填充到IssueInputParameters中,以创建问题:

      issueInputParameters.setProjectId(10100L).setIssueTypeId("8").setSummary("Test Summary").setReporterId("jobinkk").setAssigneeId("jobinkk").setDescription("Test Description").setStatusId("10010").setPriorityId("2").setFixVersionIds(10000L, 12121L);
      
    3. 确保在通过用户界面创建问题时,所有必填的值(如项目、问题类型、摘要及其他必要的字段)都已设置在IssueInputParameters中。

    4. 在这里,我们使用了测试值,但请确保将其替换为适当的值。例如,项目、问题类型 ID、优先级 ID、修复版本 ID、报告人和受理人应具有适当的值。

  3. 使用IssueService验证输入参数。

    CreateValidationResult createValidationResult = issueService.validateCreate(user, issueInputParameters);
    

    在这里,user是创建问题的用户。验证是基于用户权限进行的,如果由于权限问题或无效的输入参数导致验证失败,createValidationResult变量将包含错误!

  4. 如果createValidationResult有效,则使用IssueService创建问题。

    if (createValidationResult.isValid()) {
      IssueResult createResult = issueService.create(user,createValidationResult);
    }
    

    在这里,我们使用createValidationResult对象来创建问题,因为它已经包含了处理过的输入参数。如果结果无效,请按照以下代码中的方式处理错误:

    if (!createValidationResult.isValid()) {
      Collection<String> errorMessages = createValidationResult.getErrorCollection().getErrorMessages();
      for (String errorMessage : errorMessages) {
        System.out.println(errorMessage);
      }
      Map<String, String> errors = createValidationResult.getErrorCollection().getErrors();
      Set<String> errorKeys = errors.keySet();
      for (String errorKey : errorKeys) {
        System.out.println(errors.get(errorKey));
      }
    }
    

    在这里,如果结果无效,我们只是将错误打印到控制台。errorMessages将包含所有非字段特定的错误,如权限问题相关的错误等,而任何字段特定的错误,如输入验证错误,将出现在errors映射中,键将是字段名称。我们应该适当地处理这两种错误类型。

  5. 创建问题后,检查createResult是否有效。如果无效,适当处理。只有在 JIRA 出现严重问题时,createResult对象才会包含错误(例如,无法与数据库通信,工作流在调用验证后发生变化等)。

    if (!createResult.isValid()) {
      Collection<String> errorMessages = createResult.getErrorCollection().getErrorMessages();
      for (String errorMessage : errorMessages) {
        System.out.println(errorMessage);
      }
    }
    

    在这里,我们再次将错误打印到控制台。

  6. 如果createResult有效,那么问题已成功创建,您可以像下面这样获取它:

    MutableIssue issue = createResult.getIssue();
    

它是如何工作的……

通过使用IssueService,JIRA 现在会根据我们在 JIRA 界面上设置的规则来验证我们输入的数据,例如强制字段、权限检查、单个字段验证等。幕后,它仍然使用IssueManager类。

还有更多内容……

如前所述,在 JIRA 4.1 之前,我们需要使用IssueManager类来创建问题。它在 JIRA 4.1 及以上版本中仍然可以使用,但不推荐使用,因为它会覆盖所有验证。如果需要,下面是我们如何操作的。

使用IssueManager来创建问题

按照以下步骤操作:

  1. 使用IssueFactory类初始化问题对象:

    MutableIssue issue = ComponentManager.getInstance().getIssueFactory().getIssue();
    
  2. 设置问题对象所需的所有字段:

    issue.setProjectId(10100L);
    issue.setIssueTypeId("8");
    issue.setAssigneeId("jobinkk");
    
  3. 使用IssueManager创建问题:

    GenericValue createdIssue = ComponentManager.getInstance().getIssueManager().createIssue(user, issue);
    
  4. 处理CreateException以捕捉任何错误。

在问题上创建子任务

在这个示例中,我们将看到如何在现有问题上通过编程方式创建子任务。

如何操作……

创建子任务的步骤有两个:

  1. 创建问题对象。子任务在后台实际上就是一个问题对象。唯一的区别是它有一个关联的父问题。所以,当我们创建子任务问题对象时,我们除了通常创建普通问题时所做的操作外,还需要定义父问题。

  2. 将新创建的子任务问题与父问题关联。

让我们更详细地了解一下这些步骤:

  1. 创建子任务问题对象,类似于我们在前一个示例中创建问题的方式。在这里,构造IssueInputParameters(在适当修改setIssueTypeId()等方法之后)。

    对于这个问题,我们将使用validateSubTaskCreate方法,而不是validateCreate,该方法多了一个参数parentId

    CreateValidationResult createValidationResult = issueService.validateSubTaskCreate(user, parent.getId(), issueInputParameters);
    

    这里,父项是我们正在创建子任务的父问题对象。

  2. 在检查错误后创建问题,正如我们之前所看到的。

    if (createValidationResult.isValid()) {
      IssueResult createResult = issueService.create(user, createValidationResult);
    }
    
  3. 创建新创建的子任务问题与父问题之间的链接:

    1. 获取SubTaskManager的实例。您可以通过构造函数注入它,也可以从ComponentManager中获取它。

      SubTaskManager subTaskManager = ComponentManager.getInstance().getSubTaskManager();
      
    2. 创建子任务链接。

      subTaskManager.createSubTaskIssueLink(parent, createResult.getIssue(), user);
      
  4. 子任务现在应该已创建,并且与原始父问题相关联。

另见

  • 从插件创建问题

更新问题

在本例中,让我们看看如何编辑现有问题。

如何执行此操作...

假设我们有一个现有的问题对象。我们只需将Summary修改为一个新的摘要。以下是执行相同操作的步骤:

  1. 使用需要修改的输入字段创建IssueInputParameters对象:

    IssueInputParameters issueInputParameters = new IssueInputParametersImpl();issueInputParameters.setSummary("Modified Summary");
    

    在 JIRA 4.1.x 版本中,由于一个错误,我们需要用所有当前的字段填充IssueInputParameters,并与修改过的字段一起,以确保在更新时不会丢失现有值。然而,在 JIRA 4.2+版本中,该问题已解决,因此前面的代码仅用于修改摘要即可。

    如果你不希望保留现有值,并且只想更新问题的摘要,你可以像下面这样设置retainExistingValuesWhenParameterNotProvided标志:

    issueInputParameters.setRetainExistingValuesWhenParameterNotProvided(false);
    
  2. 使用IssueService验证输入参数:

    UpdateValidationResult updateValidationResult = issueService.validateUpdate(user, issue.getId(), issueInputParameters);
    

    这里的问题是现有的问题对象。

  3. 如果updateValidationResult有效,请更新问题:

    if (updateValidationResult.isValid()) {
        IssueResult updateResult = issueService.update(user, updateValidationResult);
    }
    

    如果无效,请像创建问题时那样处理错误。

  4. 验证updateResult并处理任何错误。如果无效,可以通过以下方式获取更新后的问题对象:

    MutableIssue updatedIssue = updateResult.getIssue();
    

删除问题

在本例中,让我们看看如何通过编程删除问题。

如何执行此操作...

假设我们有一个现有的问题对象。对于删除操作,我们也将使用IssueService类。以下是执行此操作的步骤:

  1. 使用IssueService验证删除操作:

    DeleteValidationResult deleteValidationResult = issueService.validateDelete(user, issue.getId());
    

    这里的问题是需要删除的现有问题对象。

  2. 如果deleteValidationResult有效,请调用删除操作:

    ErrorCollection deleteErrors = issueService.delete(user, deleteValidationResult);
    
  3. 如果deleteValidationResult无效,请适当地处理错误。

  4. 通过检查deleteErrors ErrorCollection 确认删除是否成功。

    if (deleteErrors.hasAnyErrors()){
      Collection<String> errorMessages = deleteErrors.getErrorMessages();
      for (String errorMessage : errorMessages) {
        System.out.println(errorMessage);
      }
    } else {
      System.out.println("Deleted Succesfully!");
    }
    

添加新的问题操作

在本例中,我们将讨论如何向问题中添加新的操作。现有的问题操作包括编辑 问题克隆 问题等,大多数时候,人们倾向于寻找类似的操作变体或完全新的操作来处理问题。

在 JIRA 4.1 之前,问题操作是通过问题操作插件模块添加的(confluence.atlassian.com/display/JIRADEV/Issue+Operations+Plugin+Module)。但从 JIRA 4.1 开始,新的问题操作是通过Web 项插件模块添加的(confluence.atlassian.com/display/JIRADEV/Web+Item+Plugin+Module)。

Web Item Plugin 模块是一个通用模块,用于在各种应用菜单中定义链接。其中一个菜单就是问题操作菜单。我们将在本书后续部分看到更多关于网页项模块以及它如何用于增强 UI 的内容;因此,在本食谱中,我们将只集中讨论如何使用网页项模块来创建问题操作。

准备工作

使用 Atlassian Plugin SDK 创建一个框架插件。

如何实现...

创建一个网页项非常简单!我们只需将其放置在合适的部分即可。JIRA 中已经定义了网页部分,如果需要,我们可以使用 Web Section 模块来添加更多部分。

让我们创建一个新操作,当我们在查看问题页面时,可以管理问题的项目。我们需要做的只是添加一个操作,将我们带到 管理 项目 页面。以下是创建新操作的步骤:

  1. 确定新操作应该放置的网页部分。

    对于问题操作,JIRA 已经定义了多个网页部分。我们可以在其中任何一个部分添加我们的新操作。以下是来自 Atlassian 文档的示意图,详细描述了每个可用的网页部分,用于问题操作:

    如何实现...

  2. 例如,如果我们想添加一个新操作,并且与 移动链接 等一起出现,我们需要将新网页项添加到 operations-operations 部分。如果你希望将它添加到顶部,和 编辑分配评论 一起显示,则该部分必须是 operations-top-level。我们可以使用 weight 属性重新排序操作。

  3. 在插件描述符中使用前面步骤中标识的部分来定义网页项模块!对于我们的示例,atlassian-plugin.xml中的模块定义将如下所示:

    <web-item key="manage-project" name="Manage Project" section="operations-operations" weight="100">
      <label>Manage Project</label>
      <tooltip>Manages the Project  in which the issue belongs </tooltip>
      <link linkId="manage-project-link">
        /secure/project/ViewProject.jspa?pid=${issue.project.id}
      </link>
    </web-item>
    

    如你所见,它有一个独特的 key 和一个可读的 name。这里的部分是 operations-operationsweight 属性用于重新排序操作,如我们之前所见,这里使用权重 100 将其放置在列表底部。

    label 是将显示给用户的操作名称。我们还可以添加一个 tooltip,它可以包含该操作的友好描述。接下来的部分,即 link 属性,是最重要的部分,因为它将我们链接到我们想要执行的操作。实际上,它只是一个链接,因此你可以将其重定向到任何地方,例如 Atlassian 网站。

    在我们的示例中,我们需要将用户带到 管理 项目区域。幸运的是,在这种情况下,我们知道要调用的操作,因为它是 JIRA 中的一个现有操作。我们需要做的就是通过传递项目 ID(作为 pid)来调用 ViewProject 操作。问题对象在查看问题页面上作为 $issue 可用,因此我们可以在链接中通过 ${issue.project.id} 获取项目 ID。

    在我们需要做新事物的情况下,我们必须自己创建一个动作并将链接指向相应的位置。我们将在本书后面了解如何创建新的动作并扩展动作。

  4. 打包插件并进行部署。

如何工作...

在运行时,你将在 查看 问题 页面上的 更多 操作 下拉菜单中看到一个新的操作,具体如下一张截图所示:

如何工作...

点击链接后,管理 项目 屏幕将如预期出现。如你所见,URL 已填充来自表达式 ${issue.project.id} 的正确 pid

另外,只需更改部分或权重,查看操作在屏幕上不同位置的显示情况!

还有更多...

在 JIRA 4.1 之前,问题 操作 模块用于创建新的问题操作。尽管此部分不在本书的范围内,但你可以在 Atlassian 文档中找到相关细节:confluence.atlassian.com/display/JIRADEV/Issue+Operations+Plugin+Module

另见

  • JIRA扩展 Webwork 动作

  • UI添加 链接

问题操作中的条件

当创建新操作时,通常需要根据权限、问题状态或其他因素来隐藏或显示它们。JIRA 允许在定义 web 项目时添加条件,当条件不满足时,web 项目将不会显示!

在这个食谱中,我们将限制仅 项目 管理员 才能使用之前创建的新问题操作。

准备中...

创建 管理 项目 问题操作,如前面所述的食谱中所解释的。

如何做到...

以下是将新条件添加到问题操作的 web 项目的步骤:

  1. 创建 condition 类。该类应该实现 com.atlassian.plugin.web.Condition 接口,但在创建问题条件时,建议扩展 com.atlassian.jira.plugin.webfragment.conditions.AbstractIssueCondition

    在扩展 AbstractIssueCondition 时,我们必须实现 shouldDisplay 方法,如下所示:

    public class AdminCondition extends AbstractIssueCondition {
      private final PermissionManager permissionManager;
    
      public AdminCondition(PermissionManager permissionManager) {
        this.permissionManager = permissionManager;
      }
    
      @Override
      public boolean shouldDisplay(User user, Issue issue, JiraHelper jiraHelper) {
            return this.permissionManager.hasPermission(Permissions.PROJECT_ADMIN, issue.getProjectObject(), user);
      }
    }
    

    在这里,如果用户具有项目上的 PROJECT_ADMIN 权限,则返回 true 值。这就是 condition 类中所需的一切。

  2. 在 web 项目中包含 condition

    <web-item key="manage-project" name="Manage Project" section="operations-operations" weight="100">
      <label>Manage Project</label>
      <tooltip>Manages the Project  in which the issue belongs </tooltip>
      <link linkId="manage-project-link">
        /secure/project/ViewProject.jspa?pid=${issue.project.id}
      </link>
      <condition class="com.jtricks.conditions.AdminCondition"/>
    </web-item>
    

    可以通过使用反转标志来反转条件,如下所示:

    <condition class="com.jtricks.conditions.AdminCondition" invert="true"/>
    

    条件元素也可以接受可选参数,如下所示:

    <condition class="com.atlassian.jira.plugin.webfragment.conditions.JiraGlobalPermissionCondition">
       <param name="permission">sysadmin</param>
    </condition>
    

    可以通过重写 init(Map params) 方法,在 condition 类中检索参数。在这里,params 是一个包含字符串键值对的映射,用来保存这些参数。在这种情况下,Map 将权限作为键,传递的值(例如 sysadmin)可以通过该键访问,然后可以用于通过或失败条件。

    例如,以下条件类中的代码将帮助你获取适当的权限类型。

    int permission = Permissions.getType((String) params.get("permission")); 
    // Permissions.SYSTEM_ADMIN in this case
    

    也可以使用conditions元素组合多个条件。conditions元素将通过逻辑“与”(默认)或“或”条件连接多个条件元素。

    例如,如果我们希望将我们的示例操作同时提供给项目管理员和 JIRA 系统管理员,我们可以使用OR条件来实现,如下所示:

    <conditions type="OR">
      <condition class="com.atlassian.jira.plugin.webfragment.conditions.JiraGlobalPermissionCondition">
      <param name="permission">sysadmin</param>
      </condition>
      <condition class="com.jtricks.conditions.AdminCondition"/>
    </conditions>
    
  3. 打包插件并进行部署。

它是如何工作的...

一旦插件部署完成,我们可以像上一章那样检查“查看问题页面”上的操作。如果你是项目管理员(或 JIRA 的系统管理员,取决于你使用的条件),你会看到该操作。如果用户没有权限,则该操作将不会显示。

例如,如果我们添加了管理 项目问题操作,并且只限制给项目管理员,再添加一个新建 管理 项目操作,且限制给项目管理员或 JIRA 的系统管理员,已登录的项目 管理员会看到两个操作,而已登录的管理员则只会看到后者操作,如下截图所示:

它是如何工作的...

处理附件

附件功能是 JIRA 中的一个有用特性,有时可以通过 JIRA API 帮助管理问题中的附件。在本教程中,我们将学习如何使用 JIRA API 处理附件。

对附件可以执行三种主要操作——创建、读取和删除。我们将在本教程中详细讲解每个操作。

正在准备中...

确保在你的 JIRA 实例中启用了附件功能。你可以通过管理 | 全局设置 | 附件来完成此操作,详细信息见 confluence.atlassian.com/display/JIRA/Configuring+File+Attachments

如何操作...

对附件的所有操作都可以通过AttachmentManager API 来执行。可以通过构造函数注入或从ComponentManager类中检索AttachmentManager,如示例所示。

AttachmentManager attachmentManager = ComponentManager.getInstance().getAttachmentManager();

创建附件

可以通过AttachmentManager上的createAttachment方法在问题上创建附件,如下所示:

ChangeItemBean changeBean = attachmentManager.createAttachment(new File(fileName), newFileName, "text/plain", user, issue.getGenericValue());

以下是参数:

  • 这里的fileName需要是文件在服务器上的完整路径。你也可以根据需求通过从客户端机器上传来创建一个文件对象。

  • newFileName是文件将附加到问题上的名称,它可以与原始文件名不同。

  • 第三个参数是文件的contentType。在本例中,我们上传的是一个文本文件,因此内容类型是 text/plain。

  • user是上传文件的用户

  • issue是文件将附加到的那个问题

如果你还希望将一组属性设置为附件的键/值对,并在特定的时间创建附件,可以使用重载方法createAttachment,该方法接受两个额外的参数:attachmentProperties,一个包含键/值属性的 Map,以及createdTime,它的类型是java.util.Date

这些属性将通过PropertySet存储在数据库中。

在问题中读取附件

AttachmentManager有一个方法可以检索附件列表,类型为com.atlassian.jira.issue.attachment.Attachment,该列表可在问题上使用。以下是如何做到这一点:

List<Attachment> attachments = this.attachmentManager.getAttachments(issue);
for (Attachment attachment : attachments) {
  System.out.println("Attachment: "+attachment.getFilename()+" attached by "+attachment.getAuthor());
}

附件对象保存了附件的所有信息,包括在创建附件时设置的任何属性。

删除附件

你需要做的就是获取需要删除的附件对象,并在AttachmentManager上调用deleteAttachment方法。

this.attachmentManager.deleteAttachment(attachment);

这里的附件是可以通过getAttachment(id)方法或通过遍历上面获取的附件列表来检索的附件。

还有更多内容...

AttachmentManager还具有其他有用的方法,如attachmentsEnabled()isScreenshotAppletEnabled()isScreenshotAppletSupportedByOS()等,用于检查相应的功能是否已启用。

查看:docs.atlassian.com/jira/latest/com/atlassian/jira/issue/AttachmentManager.html获取可用方法的完整列表。

时间跟踪和工作日志管理

时间跟踪是任何问题跟踪系统的最大优势之一。JIRA 的时间跟踪高度可配置,并提供了许多选项来管理已完成的工作和剩余时间。

尽管在 JIRA 中可以通过 JIRA UI 进行时间跟踪,但许多用户希望通过自定义页面、第三方应用程序或插件来进行。 在这个教程中,我们将学习如何使用 JIRA API 进行时间跟踪。

在我们开始之前,工作日志的每个操作,即创建、编辑或删除,都有不同的模式。每当执行其中一个操作时,我们可以通过以下方式调整剩余的工作量:

  1. 让 JIRA 自动调整剩余工作量。

    例如,如果剩余估算时间是 2 小时,并且我们记录了 30 分钟,JIRA 将自动将剩余估算时间调整为 1 小时 30 分钟。

  2. 在执行操作时输入新的剩余估算时间。

    例如,如果剩余估算时间是 2 小时,如果我们记录了 30 分钟,我们可以强制 JIRA 将剩余估算时间更改为 1 小时(而不是自动计算的 1 小时 30 分钟)。

  3. 调整剩余估算时间,或者换句话说,从剩余估算时间中减少特定的时间量。

    例如,如果剩余估算是 2 小时,且我们记录了 30 分钟的工作时间,我们可以强制 JIRA 将剩余估算减少 1 小时 30 分钟(而不是自动减少已记录的 30 分钟)。这样,剩余估算将变为 30 分钟。

  4. 保持剩余估算不变。

准备就绪...

确保按confluence.atlassian.com/display/JIRA/Configuring+Time+Tracking中的说明开启时间追踪功能。可以从 管理 | 全局 设置 | 时间 追踪 菜单中启用该功能。

如何操作...

在 JIRA 中,工作日志可以通过 WorklogService 类进行管理。它执行所有主要操作,如创建工作日志、更新工作日志或删除工作日志,并且支持我们之前看到的四种不同模式。

我们将看到如何在以下四种模式中创建工作日志,或者说记录工作:

  • 自动调整剩余估算

  • 记录工作并保留剩余估算

  • 使用新的剩余估算记录工作

  • 记录工作并按某个值调整剩余估算

自动调整剩余估算

  1. 为记录工作时间的用户创建 JIRA 服务上下文。

    JiraServiceContext jiraServiceContext = new JiraServiceContextImpl(user);
    
  2. 创建一个 WorklogInputParametersImpl.Builder 对象,以创建工作日志所需的参数。

    final WorklogInputParametersImpl.Builder builder = WorklogInputParametersImpl.issue(issue).timeSpent(timeSpent).startDate(new Date()).comment(null).groupLevel(null).roleLevelId(null);
    

    在这里,问题是记录工作时间的那个问题,timeSpent 是我们将记录的时间。timeSpent 是一个字符串,表示在 JIRA 中输入时间的格式,即 *w *d *h *m(表示周、天、小时和分钟,其中 * 可以是任何数字)。

    这里的 startDate 可以是工作开始的日期。我们还可以选择性地添加评论,并将工作日志的可见性设置为特定的组或项目角色!当工作日志对所有人可见时,请将这些参数设置为 null。

  3. 从构建器中创建 WorklogInputParameters 对象,并使用 WorklogService 对其进行验证。

    WorklogResult result = this.worklogService.validateCreate(jiraServiceContext, builder.build());
    
  4. 使用 WorklogService 创建工作日志。

    Worklog worklog = this.worklogService.createAndAutoAdjustRemainingEstimate(jiraServiceContext, result, false);
    

    在这里,如您所见,调用的方法是 createAndAutoAdjustRemainingEstimate,它将创建工作日志并自动调整问题上的剩余估算。

    该方法接受我们创建的服务上下文,经过验证的 WorklogResult 对象和一个布尔值作为输入,如果需要,它将用于触发事件。当布尔值为 true 时,将触发 工作 已记录 问题 事件。

这样,工作将会被记录到问题中。

记录工作并保留剩余估算

在这里,前三个步骤与 自动 调整 剩余 估算 部分讨论的内容相似。唯一的区别是,调用 WorklogService 的方法是 createAndRetainRemainingEstimate 而不是 createAndAutoAdjustRemainingEstimate。完整的代码如下所示:

JiraServiceContext jiraServiceContext = new JiraServiceContextImpl(user);
final WorklogInputParametersImpl.Builder builder = WorklogInputParametersImpl.issue(issue).timeSpent(timeSpent).startDate(new Date()).comment(null).groupLevel(null).roleLevelId(null);
WorklogResult result = this.worklogService.validateCreate(jiraServiceContext, builder.build());
Worklog worklog = this.worklogService.createAndRetainRemainingEstimate(jiraServiceContext, result, false);

使用新的剩余估算记录工作

这里的前两个步骤与“自动调整剩余估算”部分讨论的内容相似。

  1. 为记录工作的用户创建 JIRA 服务上下文。

    JiraServiceContext jiraServiceContext = new JiraServiceContextImpl(user);
    
  2. 创建一个 WorklogInputParametersImpl.Builder 对象,以创建工作日志所需的参数。

    final WorklogInputParametersImpl.Builder builder = WorklogInputParametersImpl.issue(issue).timeSpent(timeSpent)      .startDate(new Date()).comment(null).groupLevel(null).roleLevelId(null);
    
  3. 从构建器对象创建新的估算输入参数。

    final WorklogNewEstimateInputParameters params = builder.newEstimate(newEstimate).buildNewEstimate();
    

    在这里,我们指定了 newEstimate,它是一个类似于 timeSpent 的字符串表示。newEstimate 将作为问题上的剩余估算设置。

  4. 使用 WorklogServiceWorklogNewEstimateInputParameters 创建 WorklogResult

    WorklogResult result = this.worklogService.validateUpdateWithNewEstimate(jiraServiceContext, params);
    

    这里的结果将是 WorklogNewEstimateResult 的一个实例,将在下一步中使用!

  5. 使用 WorklogService 创建工作日志。

    Worklog worklog = this.worklogService.createWithNewRemainingEstimate(jiraServiceContext, (WorklogNewEstimateResult) result, false);
    

    在这里,使用的方法是 createWithNewRemainingEstimate,它将 newEstimate 设置为问题上的剩余估算,在使用 timeSpent 记录工作后!如您所见,结果对象转换为 WorklogNewEstimateResult

记录工作并通过值调整剩余估算

在这里,过程与上述非常相似。唯一的区别是,在构建器上使用 adjustmentAmount 方法,而不是 newEstimate,并且在 WorklogService 上使用 validateCreateWithManuallyAdjustedEstimate 来创建工作日志。此外,WorklogResultWorklogAdjustmentAmountResult 的一个实例。

代码如下:

JiraServiceContext jiraServiceContext = new JiraServiceContextImpl(user);
final WorklogInputParametersImpl.Builder builder = WorklogInputParametersImpl.issue(issue).timeSpent(timeSpent).startDate(new Date()).comment(null).groupLevel(null).roleLevelId(null);
final WorklogAdjustmentAmountInputParameters params = builder.adjustmentAmount(estimateToReduce).buildAdjustmentAmount();
WorklogResult result = worklogService.validateCreateWithManuallyAdjustedEstimate(jiraServiceContext, params);
Worklog worklog = this.worklogService.createWithManuallyAdjustedEstimate(jiraServiceContext, (WorklogAdjustmentAmountResult) result, false);

工作原理...

一旦我们使用 WorklogService API 创建或更新工作日志,更改将反映在问题的工作日志选项卡下,如下所示的屏幕截图:

工作原理...

您还可以看到时间跟踪的图形表示反映了这些更改。

当删除工作日志时,它将显示在更改历史中,如下所示:

工作原理...

还有更多内容

更新工作日志

在许多方面,更新工作日志与创建工作日志类似。在这里,我们传递要更新的 Worklog 对象的 ID,而不是创建工作日志时传递的问题 ID。当然,WorklogService 上调用的方法也不同。以下是更新给定 Worklog 的代码,其第一模式会自动调整剩余估算。

JiraServiceContext jiraServiceContext = new JiraServiceContextImpl(user);
final WorklogInputParametersImpl.Builder builder = WorklogInputParametersImpl.worklogId(worklog.getId()).timeSpent(timeSpent).startDate(new Date()).comment(null).groupLevel(null).roleLevelId(null);
WorklogResult result = this.worklogService.validateUpdate(jiraServiceContext, builder.build());
Worklog updatedLog = this.worklogService.updateAndAutoAdjustRemainingEstimate(jiraServiceContext, result, false);

如您所见,通过传递工作日志 ID 创建了一个 builder,该 ID 在所有问题中是唯一的。这里的 WorklogResult 是使用 validateUpdate 方法创建的,最终使用 updateAndAutoAdjustRemainingEstimate 方法更新工作日志。

其他模式与我们创建工作日志的方式类似。让我们快速看看如何使用新的剩余估算更新工作日志:

JiraServiceContext jiraServiceContext = new JiraServiceContextImpl(user);
final WorklogInputParametersImpl.Builder builder = WorklogInputParametersImpl.worklogId(worklog.getId()).timeSpent(timeSpent).startDate(new Date()).comment(null).groupLevel(null).roleLevelId(null);
final WorklogNewEstimateInputParameters params = builder.newEstimate(newEstimate).buildNewEstimate();
WorklogResult result = this.worklogService.validateUpdateWithNewEstimate(jiraServiceContext, params);
Worklog updatedLog = this.worklogService.updateWithNewRemainingEstimate(jiraServiceContext, (WorklogNewEstimateResult) result, false);

上面看起来非常熟悉,不是吗?它类似于使用新估算创建工作日志,只是我们调用了相应的更新方法,如前所述。

我们可以通过保留估算并按照相同方式从剩余估算中调整指定的时间量来更新工作日志。

删除工作日志

删除工作日志稍微不同,可能比创建或更新工作日志更简单,因为它不涉及构建输入参数。

自动调整剩余估算

在这里,我们只需要工作日志 ID 并创建 JIRA 服务上下文。代码如下所示:

JiraServiceContext jiraServiceContext = new JiraServiceContextImpl(user);
WorklogResult worklogResult = worklogService.validateDelete(jiraServiceContext, worklog.getId());
worklogService.deleteAndAutoAdjustRemainingEstimate(jiraServiceContext, worklogResult, false);

在这里,validateDelete方法将工作日志 ID 作为输入,并创建WorklogResult,然后在deleteAndAutoAdjustRemainingEstimate方法中使用。

删除工作日志并保留剩余估算

这与前面提到的方式非常相似,只是使用了deleteAndRetainRemainingEstimate方法,而不是deleteAndAutoAdjustRemainingEstimate

JiraServiceContext jiraServiceContext = new JiraServiceContextImpl(user);
WorklogResult worklogResult = worklogService.validateDelete(jiraServiceContext, worklog.getId());
worklogService.deleteAndRetainRemainingEstimate(jiraServiceContext, worklogResult, false);

删除带有新剩余估算的工作日志

如前所述,删除工作日志时我们不创建输入参数。相反,newEstimate用于创建WorklogResult,它是WorklogNewEstimateResult的实例,同时进行验证。代码如下:

JiraServiceContext jiraServiceContext = new JiraServiceContextImpl(user);
WorklogResult worklogResult = worklogService.validateDeleteWithNewEstimate(jiraServiceContext, worklog.getId(), newEstimate);
worklogService.deleteWithNewRemainingEstimate(jiraServiceContext, (WorklogNewEstimateResult) worklogResult, false);Deleting a worklog and adjusting the remaining estimate

这与前面提到的内容基本相同,唯一不同的是方法名称。

JiraServiceContext jiraServiceContext = new JiraServiceContextImpl(user);
WorklogResult worklogResult = worklogService.validateDeleteWithManuallyAdjustedEstimate(jiraServiceContext, worklog.getId(), adjustmentAmount);
worklogService.deleteWithManuallyAdjustedEstimate(jiraServiceContext, (WorklogAdjustmentAmountResult) worklogResult, false);

在这里,adjustmentAmount是用于增加问题上剩余估算的值。

在问题上处理评论

在这个示例中,我们将看到如何使用 JIRA API 管理问题的评论。

如何实现……

JIRA 使用CommentService类来管理问题上的评论。我们来看看三大操作——创建、编辑和删除评论。我们还将看看如何将评论的可见性限制为特定的人员组或项目角色。

在问题上创建评论

可以通过如下方式向问题添加评论:

Comment comment = this.commentService.create(user, issue, commentString, false, new SimpleErrorCollection());

在这里,commentString是我们添加的评论,user 是添加评论的用户,issue 是评论所添加的问题。第四个参数是一个布尔值,用于确定是否应派发事件。如果为真,则会抛出Issue Commented事件。

在问题上创建评论并将其限制为项目角色或组

如果需要限制评论的可见性,我们需要在CommentService类中使用重写的create方法,该方法需要传入角色 ID 和组名以及其他属性。每次只能传递其中一个。

为了将评论的可见性限制为组,我们需要将常规 配置下的Comment visibility属性设置为 & 项目角色。默认情况下,只允许限制评论仅对项目角色可见。

例如,可以通过如下方式将评论限制为

Comment comment = this.commentService.create(user, issue, commentString, group, null, false, new SimpleErrorCollection());

在这个组中,group是组名,第五个参数(null)是roleId

限制为角色的方法如下:

Comment comment = this.commentService.create(user, issue, commentString, null, roleId, false, new SimpleErrorCollection());

在这种情况下,groupnullroleId是我们需要限制评论的ProjectRole的唯一 ID。

布尔值用于派发事件,可以在这两种情况中使用。

更新评论

以下是更新评论的步骤:

  1. 从要更新的评论中创建MutableComment对象。

    MutableComment comm = this.commentService.getMutableComment(user, comment.getId(), new SimpleErrorCollection());
    
  2. 使用以下语句修改评论:

    comm.setBody("New Comment");
    

    在这里,我们更新了评论的主体,当然我们也可以更新其他属性,比如作者、组级别、角色级别等等。

  3. 使用CommentService更新评论:

    this.commentService.update(user, comm, false, new SimpleErrorCollection());
    

删除评论

可以按照以下方式删除评论:

this.commentService.delete(new JiraServiceContextImpl(user), comment, false);

comment是要删除的评论对象。

编程变更日志

跟踪问题的变更非常重要。JIRA 会存储所有在问题上进行的更改日志,并记录是谁在何时进行了更改。有时,当我们进行自定义开发时,如果插件对问题进行了更改,我们必须手动更新变更 历史

变更历史作为变更组记录,这些变更组由用户在同一时刻进行的一个或多个变更项组成。每个变更项都是对单一字段的更改。

在这个教程中,我们将学习如何使用 JIRA API 在问题上添加变更日志。

如何实现...

在 JIRA 中,每个更改项都作为ChangeItemBean创建。ChangeItemBean有两种不同的类型—一种是针对系统字段,其中字段类型为ChangeItemBean.STATIC_FIELD,另一种是针对自定义字段,其中字段类型为ChangeItemBean.CUSTOM_FIELD

以下是添加变更历史的步骤。

  1. 为每个需要记录变更的项目创建一个ChangeItemBean

    ChangeItemBean changeBean = new ChangeItemBean(ChangeItemBean.STATIC_FIELD, IssueFieldConstants.SUMMARY,  "Old Summary", "New Summary");
    

    这里,第一个属性是fieldType,第二个属性是字段名称。对于类型为ChangeItemBean.STATIC_FIELD的系统字段,名称可以从IssueFieldConstants类中获取。例如,IssueFieldConstants.SUMMARY表示问题摘要。

    第三和第四个参数分别是字段的

    正如我们所知,一些 JIRA 字段有 ID 值和字符串值。例如,问题状态有状态名称和相应的状态 ID。在这种情况下,我们可以使用一个重载构造函数,同时传入旧的 ID 和新的 ID,如下所示。

    ChangeItemBean changeBean = new ChangeItemBean(ChangeItemBean.STATIC_FIELD, IssueFieldConstants.STATUS,"1", "Open", "3", "In Progress");
    

    对于自定义字段,我们使用字段类型ChangeItemBean.CUSTOM_FIELD和自定义字段名称。其他部分保持不变。

    ChangeItemBean changeBean = new ChangeItemBean(ChangeItemBean.CUSTOM_FIELD, "My Field",  "Some Old Value", "Some New Value");
    

    值得注意的是,当fieldTypeChangeItemBean.CUSTOM_FIELD时,字段名称可以被修改为任何值。当你希望以编程方式添加与字段无直接关联的变更日志时,这可能是一个有用的功能。比如,添加一个子任务!

    ChangeItemBean changeBean = new ChangeItemBean(ChangeItemBean.CUSTOM_FIELD, "Some Heading", "Some Old Value", "Some New Value");
    
  2. 创建一个变更持有者,并将变更项添加到其中。

    IssueChangeHolder changeHolder = new DefaultIssueChangeHolder();
    changeHolder.addChangeItem(changeBean);
    
  3. 使用ChangeLogUtils类将changeHolder中的项目创建并存储为changelog

    GenericValue changeLog = ChangeLogUtils.createChangeGroup(user, issue, issue, changeHolder.getChangeItems(),  false);
    

    这里用户是进行更改的用户。第二个和第三个参数分别是原始问题和更改后的问题。如果更改项是显式创建并添加到changeHolder中,你可以将两者设置为相同。

    但是,如果我们使用 setter 方法修改问题,一个更简单的方法是传递原始问题对象与修改后的问题对象(调用 setter 方法后的对象),并将最后一个参数设置为true,这样就可以决定是否需要从前后的对象生成更改项列表。在这种情况下,我们不需要显式地创建changeItems,因此第三个参数可以是一个空列表。如果需要,也可以将附加的changeItems作为第三个参数传递,在这种情况下,传递的changeItems和由问题修改前后生成的changeItems都将被创建!

如何工作...

一旦更改日志添加完成,它们将显示在问题的更改日志面板中,如下图所示:

如何工作...

请注意,尽管没有名为Some Heading的字段,但高亮显示的更改 日志已被添加。此外,还可以看到状态字段的 ID 和名称均被显示!

编程问题链接

问题链接是 JIRA 中的另一个重要功能。它帮助我们定义问题之间的关系。在本教程中,我们将学习如何使用 JIRA API 创建问题之间的链接以及如何断开这些链接!

在我们开始之前,问题链接类型有一个“入向”和一个“出向”描述。对于每个问题链接,都有一个源问题和一个目标问题。从源问题出发,我们可以通过查找出向链接来查找目标问题。同样,从目标问题出发,我们可以通过查找入向链接来查找源问题。

准备就绪...

确保在 JIRA 中启用了问题链接功能并且创建了有效的链接类型。可以通过管理 | 全局 设置 | 问题 链接来完成此操作,具体内容请参见confluence.atlassian.com/display/JIRA/Configuring+Issue+Linking

如何操作...

问题链接通过IssueLinkManager类在 JIRA 中进行管理。以下是创建两个给定问题之间问题链接的步骤:

  1. 获取我们要创建的链接类型的IssueLinkType对象。可以通过IssueLinkTypeManager类来获取。IssueLinkTypeManager类可以从ComponentManager中获取,或者可以在构造函数中注入。

    IssueLinkTypeManager issueLinkTypeManager = ComponentManager.getInstance().getComponentInstanceOfType(IssueLinkTypeManager.class);
    IssueLinkType linkType = issueLinkTypeManager.getIssueLinkTypesByName("Duplicate").iterator().next();
    

    在这里,我们获取了Duplicate问题链接类型。即使getIssueLinkTypesByName方法返回的是一个集合,具有相同名称的链接也只有一个。

  2. 使用IssueLinkManager类创建问题链接。IssueLinkManager类也可以从ComponentManager类中获取,或者在构造函数中注入。

    IssueLinkManager issueLinkManager = ComponentManager.getInstance().getIssueLinkManager();
    issueLinkManager.createIssueLink(sourceIssue.getId(), destIssue.getId(), linkType.getId(), null, user);
    

    在这里,我们传递源和目标问题的 ID,按所述顺序,以及链接类型 ID。第四个参数是序列,类型为long,用于在用户界面上对链接进行排序。user是执行链接操作的用户。

还有更多…

现在我们来看看如何删除它们或仅显示链接。

删除问题链接

以下是步骤:

  1. 按照之前的方式检索IssueLinkType

    IssueLinkTypeManager issueLinkTypeManager = ComponentManager.getInstance().getComponentInstanceOfType(IssueLinkTypeManager.class);
    IssueLinkType linkType = issueLinkTypeManager.getIssueLinkTypesByName("Duplicate").iterator().next();
    
  2. 使用IssueLinkManager类获取要删除的IssueLink

    IssueLink issueLink = issueLinkManager.getIssueLink(sourceIssue.getId(), destIssue.getId(), linkType.getId());
    
  3. 这里的sourceIssuedestIssue分别是源问题和目标问题。

  4. 使用IssueLinkManager类删除链接。

    issueLinkManager.removeIssueLink(issueLink, user);
    

在问题上检索问题链接

我们可以使用IssueLinkManager类中的不同方法检索问题的内链或外链,或者所有已链接的问题。

所有内链可以按照如下方式检索:

List<IssueLink> links = issueLinkManager.getInwardLinks(issue.getId());
for (IssueLink issueLink : links) {
  System.out.println(issueLink.getIssueLinkType().getName()+": Linked from "+issueLink.getSourceObject().getKey());
}

这里,issue是目标对象,我们获取所有内链问题并显示源问题键。

类似地,外链可以按照如下方式检索:

links = issueLinkManager.getOutwardLinks(issue.getId());
for (IssueLink issueLink : links) {
  System.out.println(issueLink.getIssueLinkType().getName()+": Linked to "+issueLink.getDestinationObject().getKey());
}

这里,问题是源对象,我们获取所有的外部问题链接并显示目标问题键。

所有已链接的问题可以通过一个方法检索,如下所示:

LinkCollection links = this.issueLinkManager.getLinkCollection(issue, user);
Collection<Issue> linkedIssues = links.getAllIssues();

问题链接验证

在某些场景中,我们可能会遇到需要进行额外验证的情况。本文将简要介绍如何通过扩展现有的 JIRA 链接问题动作来添加一些额外的验证。

准备就绪...

使用 Atlassian Plugin SDK 创建骨架插件。建议在继续之前阅读扩展 JIRA 动作配方。

如何操作...

正如我们在扩展 JIRA 动作时所见,我们在这里需要做的只是创建一个新的 webwork 动作,扩展现有的 JIRA 动作并重写所需的方法。在这个特定的案例中,我们将重写doValidation()方法来做一些额外的验证。

比如,我们假设想要限制链接所有新功能类型的问题。以下是执行此操作的步骤。

  1. atlassian-plugin.xml中添加一个新的 webwork 模块,包含一个新的动作类,并且使用与 JIRA 的链接动作相同的别名LinkExistingIssue。这样,我们的新动作类将在链接问题时执行。

    <webwork1 key="jtricks-link-issue-details" name="JTricks Link Issue Details" > 
      <actions>
        <action name="com.jtricks.JTricksLinkExistingIssue" alias="LinkExistingIssue">
        <view name="error">/secure/views/issue/linkexistingissue.jsp</view> 
        <view name="input">/secure/views/issue/linkexistingissue.jsp</view>
        </action>
      </actions>
    </webwork1>
    
  2. 创建一个新类,扩展现有的动作类。

    public class JTricksLinkExistingIssue extends LinkExistingIssue {
      ...
    }
    
  3. 重写doValidation()方法以添加额外验证。

    @Override
    protected void doValidation() {
      super.doValidation();
      // Custom Validation
    }
    
  4. 根据需要添加自定义验证。在我们的示例中,如果任何选中的链接问题是新功能类型,我们会抛出错误。选中的问题可以通过getLinkKey()方法找到,该方法返回一个包含选中问题键的字符串数组。

    List<String> invalidIssues = new ArrayList<String>();
    for (String key : getLinkKey()) {
      MutableIssue issue = this.issueManager.getIssueObject(key);
      if (issue.getIssueTypeObject().getName().equals("New Feature")) {
        invalidIssues.add(key);
      }
    }
    if (!invalidIssues.isEmpty()) {
      addErrorMessage("Linking not allowed to New Features:" + getString(invalidIssues));
    }
    
    private String getString(List<String> invalidIssues) {
      StringBuffer invalidIssue = new StringBuffer("{ ");
      for (String key : invalidIssues) {
        invalidIssue.append(key + " ");
      }
      invalidIssue.append("}");
      return invalidIssue.toString();
    }
    
  5. 如你所见,我们在这里做的只是检查问题键,如果问题类型是新功能,则将其标记为无效。如果无效,我们会对这些无效的键抛出错误。

  6. 打包插件并进行部署。

我们可以使用相同的方法来添加额外的验证。

注意

动作只能被重写一次。必须小心不要在其他插件(可能是第三方插件)中再次重写,因为只有一个动作会被选中。

它是如何工作的...

假设我们要链接到三个现有问题,其中两个是功能。插件部署后,我们会看到如下错误:

它是如何工作的...

另见

  • 在 JIRA 中扩展 Webwork 动作

在克隆时丢弃字段

在 JIRA 中克隆问题是一种简单的复制现有问题的方法。克隆时,会创建一个与原始问题完全相同的新问题,所有字段的值与原始问题一致,除了少数几个特殊字段。这些特殊字段包括created dateupdated dateissue keystatus 等等。

但是,除了 JIRA 已选择的特殊字段外,我们可能还希望在克隆问题时忽略其他几个字段。比如,一个独特的自定义字段呢?我们当然不希望在克隆时复制它。

这里有一个简单的方法,可以在克隆问题时丢弃任何这样的字段。

准备工作...

使用 Atlassian Plugin SDK 创建一个骨架插件。建议在继续之前阅读扩展 JIRA 动作的配方。

如何做到...

正如我们在上一个配方中扩展 JIRA 动作时所看到的,我们在这里要做的就是创建一个新的 webwork 动作,扩展现有的 JIRA 克隆动作并覆盖所需的方法。在这个特定的案例中,我们将覆盖setFields()方法,以移除我们关心的特定字段的克隆!

举个例子,假设我们想避免克隆一个名为Test Number的唯一数字字段。以下是需要遵循的步骤:

  1. atlassian-plugin.xml中添加一个新的 webwork 模块,包含一个新的动作类,并使用与 JIRA 克隆动作相同的别名CloneIssueDetails。我们这样做后,在克隆问题时,将执行新的动作类。

    <webwork1 key="jtricks-link-issue-details" name="JTricks Link Issue Details" >	
      <actions>
        <action name="com.jtricks.JTricksCloneIssueDetails" alias="CloneIssueDetails">
          <view name="input">/secure/views/cloneissue-start.jsp</view>
          <view name="error">/secure/views/cloneissue-start.jsp</view>
        </action>
      </actions>
    </webwork1>
    
  2. 创建一个新的类,扩展现有的动作类。

    public class JTricksCloneIssueDetails extends CloneIssueDetails{
      ...
    }
    
  3. 覆盖setFields()方法为我们不想克隆的字段设置null值。

    @Override
    protected void setFields() throws FieldLayoutStorageException {
      super.setFields();
      // Set null values for interested fields here	
    }
    
  4. 添加代码以设置null值。在我们的例子中,我们为Test Number自定义字段设置了空值。

    CustomField customField = customFieldManager.getCustomFieldObjectByName("Test Number");
    getIssueObject().setCustomFieldValue(customField, null);
    

    在这里,我们使用getIssueObject方法获取克隆后的问题,并为自定义字段设置空值。如果字段名不是唯一的,请不要忘记使用getCustomFieldObject方法,并传入自定义字段 ID!

    如果我们想为像版本修复这样的系统字段设置空值,方法是相同的。

    getIssueObject().setFixVersions(null);
    
  5. 打包插件并部署。

    注意

    一个动作只能被覆盖一次。必须小心避免在另一个插件中再次覆盖它(可能是第三方插件),因为只有一个会被选中。

它是如何工作的...

一旦调用克隆操作,我们创建的新动作将被执行。克隆操作会创建一个新的问题对象,并将其字段的值从原始问题复制过来。这是在setFields方法中完成的。

从逻辑上讲,覆盖此方法并为我们不想克隆的字段设置 null 值是合理的。如上所示,首先执行的是超类中的 setFields 方法,即 JIRA 内置的类。执行该方法后,可以使用 getIssueObject 方法获取的新问题对象已填充所有值。我们只需通过将某些值重置为 null 来重置它们。

另见

  • 扩展 JIRA 中的 Webwork 操作

针对问题字段的 JavaScript 技巧

JIRA 提供了许多选项来管理问题上的各类字段。字段配置方案、屏幕方案等帮助 JIRA 管理员为不同的 issue 类型和项目,显示或隐藏字段、标记字段为必填项等。

无论这些方案多么可配置,仍然有一些地方需要我们进行自定义开发。例如,如果我们需要根据另一个字段的值来显示或隐藏字段,JIRA 并没有内置的选项来实现这一点。

那么,处理这个问题的最佳方法是什么呢?我们始终可以创建一个新的复合自定义字段,让多个字段根据彼此的行为来驱动。但可能更简单的方式——不需要开发插件——是通过 JavaScript 来实现。而且,为了让事情更好,JIRA 提供了可以用来编写整洁 JavaScript 代码的 jQuery 库!

然而,使用 JavaScript 来处理字段行为可能会带来一些问题。它将行为限制在浏览器中,是客户端的,并且取决于是否启用了 JavaScript。但鉴于其优势和易用性,大多数用户都更愿意使用它。在本食谱中,我们将展示一个小例子,使用 JavaScript 根据问题的优先级值来显示或隐藏自定义字段的值!

如何实现...

假设我们有一个名为 Why Critical? 的自定义字段。该字段只有在问题的优先级为 Critical 时才会显示。

以下是使用 JavaScript 实现此功能的简单步骤:

  1. 编写 JavaScript 来实现该功能。

    在我们的例子中,我们只需要在优先级为 Critical 时显示 Why Critical 字段。让我们作为一个示例编写 JavaScript 来实现这一功能!

    1. 找出优先级的 ID 值。我们可以通过查看编辑优先级时的 URL 或从 JIRA 数据库中的 priority 表中获取它。

    2. 找出自定义字段的 ID。我们也可以通过类似的方式获取它,要么通过在编辑自定义字段时查看 URL,要么从customfield表中获取!

    3. 编写 JavaScript 来根据优先级值显示或隐藏字段。在这里,我们使用 JIRA 的 jQuery 库,它具有一个预定义的命名空间 AJS,是 Atlassian JavaScript 的简写!

      <script type="text/javascript">
      (function($){
        $(document).ready(function(){
          var priority = document.getElementById('priority');
          hideOrShow(priority.value);
          priority.onchange=function() {
            hideOrShow(priority.value);
          };
        });
      
        function hideOrShow(priorityVal){
          if (priorityVal == '2'){
            AJS.$("#customfield_10170").closest('div.field-group').show();
          } else {
            AJS.$("#customfield_10170").closest('div.field-group').hide();
          }
        }
      })(AJS.$);
      </script>
      
    4. 这里 10170 是 customfieldid,因此 customfield_10170 表示唯一的自定义字段 ID!另外,2 是优先级系统字段的 ID。

      在这个示例中,我们创建了一个页面加载事件,脚本查看优先级值并设置围绕自定义字段的div的可见性为隐藏或显示。

      以下部分捕获了页面加载事件,其中自定义字段处于编辑模式。

      (function($){
      $(document).ready(function(){
          ...
        });
      })(AJS.$);
      
      

      以下代码显示了当优先级为 2 时的字段。

      (AJS.$("#customfield_10170").closest('div.field-group').show();
      

      对于每个其他优先级值,围绕字段的最接近的div会被隐藏。

  2. 将上述 JavaScript 添加到自定义字段的描述中。

字段行为将在 JavaScript 添加到字段描述后,下一次重新加载时生效。

它是如何工作的...

每当字段在编辑模式下通过 velocity 视图渲染时,字段描述会与其中的所有 JavaScript 代码一起执行!

一旦脚本被添加到相关的字段配置屏幕中,字段将不会出现在关键以外的优先级值下,如下一个截图所示:

它是如何工作的...

在这里,优先级重大(值为 3),因此字段为什么 关键? 不可用。但一旦优先级更改为关键,我们可以看到该字段重新出现在页面上。

它是如何工作的...

现在,JavaScript 可以被修改以执行许多其他有用的操作!不要忘记根据你的需求修改脚本,特别是根据你的浏览器和 JIRA 版本。

第八章。自定义 UI

在本章中,我们将覆盖:

  • 更改基本的外观和感觉

  • 在 UI 中添加新的网页部分

  • 在 UI 中添加新的网页项

  • 为网页片段添加条件

  • 为网页片段创建新的 Velocity 上下文

  • 在顶部导航栏中添加新的下拉菜单

  • 动态创建网页项

  • 查看 问题屏幕中添加新的标签页

  • 浏览 项目屏幕中添加新的标签页

  • 使用片段创建项目标签面板

  • 浏览 版本屏幕中添加新的标签页

  • 浏览 组件屏幕中添加新的标签页

  • 扩展 webwork 操作以添加 UI 元素

  • 在问题上显示动态通知/警告

  • 查看 问题页面中重新排序问题操作

  • 查看 问题页面中重新排序字段

简介

JIRA 的一个优点是它拥有一个简单但强大的用户界面。从 3.13.x 到 4.1.x,用户界面发生了很大的变化,且它仍然是一个能让用户满意、插件开发者感兴趣的界面。

虽然现有的 JIRA 界面适合许多人使用,但也有一些情况需要我们修改界面的一部分,添加新的 UI 元素、删除一些元素等等。

通常,当我们想到修改一个 Web 应用程序的用户界面时,首先想到的就是去修改 JSP、VM 以及其他相关文件。虽然在某些情况下,JIRA 确实需要这么做,但许多 UI 的更改可以在不接触 JIRA 代码的情况下进行。JIRA 通过多个与 UI 相关的插件模块帮助我们实现这一点。

在本章中,我们将研究如何利用各种可用的插件模块来增强 JIRA UI 的不同方法,并在某些情况下,通过修改 JSP 或其他相关文件来实现。

请注意,外观和感觉只能通过修改 CSS 文件和其他相关模板来大幅度改变。但是这里我们讨论的是在不实际修改核心 JIRA 文件或仅对其进行少量修改的情况下,在 UI 的各个部分添加新的网页片段,例如新的部分和链接。如果我们修改 JIRA 文件,需要注意的是,在不同 JIRA 版本之间维护这些文件、启用或禁用这些更改等,将会非常困难,值得考虑!

更改基本的外观和感觉

如前所述,JIRA 的外观和感觉的大幅变化只能通过修改 CSS 文件、JSP、模板和其他相关工具来实现。但是,JIRA 允许管理员进行一些简单的更改,例如更改 Logo、配色方案等,这些更改可以通过一些简单的配置来实现。在本例中,我们将看到如何轻松进行这些更改的一些示例。

主要有四个方面可以配置来改变 JIRA 的外观:

  • Logo:可以理解的是,这是每个人都希望更改的内容。

  • 颜色:JIRA 拥有围绕蓝色主题的精美配色方案。但我们可以轻松地更改这些颜色以符合我们的口味,或者说是公司的口味!

  • 小工具颜色:对于 JIRA 中的每个小工具,我们可以从预定义的颜色集中选择不同的颜色。我们可以通过简单的配置轻松更改预定义的颜色列表。

  • 日期和时间格式:JIRA 中的日期和时间格式可以轻松修改以满足我们的需求,前提是它是 Java 的 SimpleDateFormat 支持的有效格式 (download.oracle.com/javase/1.4.2/docs/api/java/text/SimpleDateFormat.html)。

如何操作...

以下是更改基本 JIRA 外观和感觉的步骤。

  1. 以管理员身份登录 JIRA。

  2. 导航到 管理 | 全局 设置 | 外观 感觉

  3. 点击 编辑 配置

  4. 根据需要进行相应的更改:

    1. Logo:将您的新 logo 与 JIRA WAR 一起打包,或者将其放在 JIRA 安装目录下的 images 文件夹或其他有效目录中。将新 logo 的 URL 作为相对路径引用到 JIRA 安装目录中的新 logo。

      例如,/images/logo/mynewlogo.png 将指向 images/logo 文件夹下的 mynewlogo.png 图片。根据需要输入新的 logo 宽度或高度。

    2. 颜色:如果需要更改颜色方案,请指定感兴趣颜色的十六进制表示法(HEX 值)。

    3. 小工具 颜色:在这里,同样指定感兴趣颜色的十六进制表示法(HEX 值),以便小工具用户可以从新的颜色集中选择。

    4. 日期 和时间 格式:输入新的日期和时间格式,前提是它是 Java 的 SimpleDateFormat 支持的有效格式 (download.oracle.com/javase/1.4.2/docs/api/java/text/SimpleDateFormat.html)。

  5. 点击 更新

重复该过程,直到达到预期的结果。我们始终可以通过点击 重置 默认值 来恢复到默认设置,编辑配置时使用。

通过一些简单的更改,JIRA UI 看起来可以与平时大不相同。以下截图就是一个小示例:

如何操作...

在这种情况下,查看 问题 页面将如下图所示:

如何操作...

这是一个小而强大的改变!

在 UI 中添加新的网页部分。

网页 片段 是 JIRA 网页界面中特定位置的链接或链接部分。它可以是 JIRA 顶部导航栏中的一个菜单、一组新的问题操作,或者 管理员 UI 部分中的一个新区域。

在 JIRA 中添加新的 web fragment 有两种类型的插件模块,即 Web Section 插件模块和 Web Item 插件模块。Web Section 是一组链接,这些链接会一起显示在 JIRA 用户界面的某个特定位置。它可能是问题操作栏上的一组按钮,或者是由分隔线分开的链接组。

在本示例中,我们将看到如何将一个新的 web section 添加到 JIRA 中。

如何操作……

以下是将新的 web section 添加到 JIRA 的步骤:

  1. 确定位置,即新 sections 应该添加的地方。

    JIRA 在其用户界面中有许多已知的位置,并允许我们在这些位置添加新的 web sections。所有可用位置的完整列表可以在 confluence.atlassian.com/display/JIRA/Web+Fragments 查找。

  2. 将新的 web-section 模块添加到 atlassian-plugin.xml 中。

    <web-section key="jtricks-admin-section" name="JTricks Section" location="system.admin" i18n-name-key="webfragments.admin.jtricks.section" weight="900">
      <label>J Tricks</label>
      <description>J Tricks Section Descitption</description>
      <tooltip>J Tricks - Little JIRA Tricks</tooltip>
    </web-section>
    
  3. 与所有其他插件模块一样,它有一个唯一的模块 key。这里,web-section 元素的另外两个重要属性是 locationweightlocation 定义了 section 应该出现在 UI 中的位置,而 weight 定义了它应该出现的顺序。

    在上面的示例中,location 是 system.admin,这将在管理屏幕下创建一个新的 web section,就像现有的部分:ProjectGlobal Settings 等一样。

  4. web-section 模块也有一组子元素。conditionconditions 元素可以用于定义一个或多个条件,详细信息将在以下示例中展示。context-provider 元素可以用来添加一个新的上下文提供者,从而定义 web section 的 velocity 上下文。label 是将显示给用户的内容。param 是另一个元素,用于定义键值参数,若我们希望从 UI 中使用额外的自定义值,它会非常有用。resource 元素可用于包含如 JavaScript 或 CSS 文件等资源文件,tooltip 元素则为该部分提供一个工具提示。label 是唯一的必需元素。

    元素如 labeltooltip 可以拥有可选的键值参数,如以下代码所示:

    <label key="some.valid.key">
      <param name="param0">$somevariable</param>
    </label>
    

    如你在示例中看到的,label 接受一个 key/value 参数,其中值是动态从 velocity 变量中填充的。param 将作为 {0} 被传递到文本中,并在标签中替代该位置。这里,参数允许使用 Java 的 MessageFormat 语法将值插入到 label 中,相关详情可以在 http://download.oracle.com/javase/7/docs/api/java/text/MessageFormat.html 查找。参数名必须以 param 开头,并按字母顺序映射到格式字符串中的替代位置,即 param0 对应 {0}param1 对应 {1}param2 对应 {2},以此类推。

  5. 部署插件。

它是如何工作的……

插件部署完成后,我们可以看到在 JIRA 的管理屏幕中创建了一个新部分,如下图所示。Web 项目将在下一个实例中详细解释。

它是如何工作的...

我们可以通过更改location属性在许多不同的位置添加部分。如果我们仅将位置更改为opsbar-operations,新的部分将出现在查看 问题页面,如下图所示。

Web 项目的部分属性也必须更改,以匹配新的位置,即opsbar-operations/jtricks-admin-section

它是如何工作的...

请注意,Web 部分标签可能并不总是可见,因为在某些情况下,部分只是用来将链接进行分组。例如,在问题操作的情况下,部分仅用于将链接分组,如前所示。

另见

  • UI 中 添加 新的 Web 项目

在 UI 中添加新的 Web 项目

一个Web 项目是一个可以在 JIRA UI 的多个位置添加的新链接。链接通常会放置在一个Web 部分下。链接可以直接指向一个 URL,也可以用于触发一个动作。在这个实例中,我们将展示如何将一个新的 Web 项目添加到 JIRA 中。

如何操作...

以下是在 JIRA 中添加新 Web 项目的步骤:

  1. 确定新链接应添加的Web 部分

  2. 我们已经看到如何创建一个新的 Web 部分。然后,链接被添加到上述创建的部分中,或添加到预定义的 JIRA 部分中。如果它是一个非部分化位置,我们可以直接将链接添加到该位置。对于部分化的位置,它是位置key,后面跟着一个斜杠('/'),然后是该链接应显示的 Web 部分的key

    例如,如果我们想在之前创建的 Web 部分中放置一个链接,则该部分元素的值将为system.admin/jtricks-admin-section

  3. 将新的 Web 项目模块添加到atlassian-plugin.xml中。

    <web-item key="jtricks-admin-link" name="JTricks Link" section="system.admin/jtricks-admin-section" i18n-name-key="webfragments.admin.jtricks.item" weight="10">
      <label>J Tricks Website</label>
      <link linkId="jtricks.admin.link">http://www.j-tricks.com</link>
    </web-item>
    

    一个 Web 项目模块还拥有一个唯一的key。Web 项目的另外两个重要属性是sectionweightsection定义了链接所在的 Web 部分,如前所述,weight定义了链接显示的顺序。

    一个 Web 项目还包含所有 Web 部分的元素:condition/conditionscontext-providerdescriptionparamresourcetoolitp。此外,Web 项目还有一个link元素,用于定义 Web 项目的链接目标。链接可以是一个动作、一个直接链接等,还可以通过 velocity 参数动态创建,如下所示的示例:

    <link linkId="create_link" absolute="false">/secure/CreateIssue!default.jspa</link>
    <link linkId="google_link">http://www.google.com</link>
    <link linkId="profile_link" absolute="false">/secure/ViewProfile.jspa?name=$user.name</link>
    

    在第三个示例中,user是一个在 velocity 上下文中可用的变量!

    当我们需要在链接旁边添加图标时,会使用icon元素:

    <icon height="16" width="16">
      <link>/images/avatar.gif</link>
    </icon>
    
  4. 部署插件。

如何操作...

一旦插件部署完成,我们可以看到一个新的 Web 项目显示在我们之前创建的 Web 部分下,位于 JIRA 的Admin屏幕中。

它是如何工作的...

通过更改部分属性,我们可以在不同位置添加项目。在前一个教程中创建新问题操作时,我们已经看到了一个示例。

另请参阅

  • 在 UI 中添加 新的 Web 部分

添加 Web 片段的条件

正如我们在前面的教程中看到的,添加 Web 片段非常简单。但是,工作不总是停留在那里。在许多情况下,我们希望根据一组条件限制 Web 项目。

例如,对于具有问题编辑权限的人员,编辑问题链接应该显示。仅当用户是 JIRA 管理员时,才应显示管理员链接。在本教程中,让我们看看如何实现显示 Web 片段的条件。

如何实现……

可以向 Web 部分或 Web 项目添加一个或多个条件。在后一种情况下,使用conditions元素,此时包含一组condition/conditions元素和一个type属性。类型属性可以是逻辑ANDOR

例如,以下条件指定用户在可以看到具有以下条件的 Web 片段之前,应在项目中具有admin权限或use权限:

<conditions type="OR">
  <condition class="com.atlassian.jira.plugin.webfragment.conditions.JiraGlobalPermissionCondition">
    <param name="permission">admin</param>
  </condition>
  <condition class="com.atlassian.jira.plugin.webfragment.conditions.JiraGlobalPermissionCondition">
    <param name="permission">use</param>
  </condition>
</conditions>

权限的可能值包括adminusesysadminprojectbrowsecreateeditscheduleissueassignassignableattachresolveclosecommentdeleteworkworklogdeleteallworklogdeleteownworklogeditallworklogeditownlinksharefiltersgroupsubscriptionsmovesetsecuritypickusersviewversioncontrolmodifyreporterviewvotersandwatchersmanagewatcherlistbulkchangecommenteditallcommenteditowncommentdeleteallcommentdeleteown

让我们考虑一个简单的例子,介绍如何编写条件并基于它显示 Web 项目。在这个例子中,我们将在顶部导航栏中显示一个 Web 项目,前提是用户已登录并且属于jira-developer组。以下是步骤:

  1. 编写condition类。该类应扩展AbstractJiraCondition类并重写以下抽象方法。

    public abstract boolean shouldDisplay(User user, JiraHelper jiraHelper);
    
  2. 在我们的示例中,我们只需检查用户不为空并且是jira-developers组的成员即可。该类的实现如下:

    public class DeveloperCondition extends AbstractJiraCondition {
      @Override
      public boolean shouldDisplay(User user, JiraHelper jiraHelper) {
        return user != null && user.getGroups().contains("jira-developers");
      }
    }
    
  3. web-item中添加新的条件类:

    <web-item key="jtricks-condition-menu" name="JTricks Condition Menu" section="system.top.navigation.bar" weight="160">
    
      <description>J Tricks Web site with condition</description>
      <label>JTricks Conditional Menu</label>
      <tooltip>J Tricks Web site</tooltip>
      <link linkId="jtricks-condition-menu">http://www.j-tricks.com</link>
      <condition class="com.jtricks.conditions.DeveloperCondition"/>
    </web-item>
    

    如您所见,此处的部分是system.top.navigation.bar,它将在顶部导航栏上放置新链接。但仅当DeveloperCondition条件返回true时,该链接才可见。

    我们可以使用invert标志轻松地反转条件,如下所示:

    <condition class="com.jtricks.conditions.DeveloperCondition" invert="true"/>
    

    如果用户未登录或不属于 JIRA 开发者组,这将显示链接!

  4. 部署插件。

如何运作……

一旦插件部署完成,我们可以看到新的JTricks 条件 菜单仅在用户登录并且属于 JIRA 开发者组时才会出现在顶部导航栏中。

以下截图展示了一个已登录且属于 JIRA 开发者组的用户的仪表盘:

如何工作...

如果用户未登录,则菜单不会显示,如下截图所示。在这种情况下,我们只会看到没有定义条件的网页项!

如何工作...

为网页片段创建新的 velocity 上下文

正如我们在之前的教程中提到的,在构建 JIRA 网页片段时,确实可以添加 velocity 变量。JIRA 默认支持一系列变量,包括userreqbaseurl等。有关这些变量的完整列表和详细信息,请参见confluence.atlassian.com/display/JIRADEV/Web+Fragments#WebFragments-VelocityContext

在这个教程中,我们将看到如何使用context-provider元素向 velocity 上下文添加更多变量。

如何操作...

context-provider元素会将新的内容添加到网页区域和网页项模块的 Velocity 上下文中。每个项目只能添加一个context-provider。以下步骤展示了如何使用上下文提供者:

  1. 创建新的ContextProvider类。

    该类必须实现com.atlassian.plugin.web.ContextProvider。为了简化操作,只需扩展AbstractJiraContextProvider类,并覆盖其中的以下抽象方法:

    public abstract Map getContextMap(User user, JiraHelper jiraHelper);
    

    如果你想将用户的全名作为一个单独的变量添加到 velocity 上下文中,下面是class的代码样式。

    public class UserContextProvider extends AbstractJiraContextProvider {
      @Override
      public Map getContextMap(User user, JiraHelper helper) {
        return EasyMap.build("userName", user.getFullName());
      }
    }
    

    请注意,$user变量在网页片段的 velocity 上下文中已经可用,因此可以通过$user.getFullName()轻松获取用户的全名。这只是一个简单的示例,展示了如何使用上下文提供者。

  2. 在构建网页区域/项时,适当地使用添加到 velocity 上下文中的变量。

    在这个示例中,让我们创建一个新的网页区域,在管理员部分显示用户的全名,并且包含一个链接到用户网站的网页项。

    <web-section key="jtricks-admin-context-section" name="JTricks Context Section" location="system.admin" i18n-name-key="webfragments.admin.context.jtricks.section" weight="910">
      <label>$userName</label>
      <context-provider class="com.jtricks.context.UserContextProvider" />
    </web-section>
    
    <web-item key="jtricks-admin-context-link" name="JTricks Context Link" section="system.admin/jtricks-admin-context-section" i18n-name-key="webfragments.admin.context.jtricks.item" weight="10">
      <label>Website</label>
      <link linkId="jtricks.admin.context.link">http://www.j-tricks.com</link>
    </web-item>
    

    如你所见,网页区域在它的标签中引用了$userName

  3. 部署插件。

如何工作...

一旦插件部署完成,我们可以看到新的网页区域已在 JIRA 管理员 UI 下创建,如下截图所示。$userName变量将动态替换为当前用户的全名。

如何工作...

在顶部导航栏添加一个新的下拉菜单

在这个教程中,我们将展示如何快速使用网页区域和网页项模块,在 JIRA 的顶部导航栏中添加一个新的下拉菜单。

如何操作...

在这里,我们首先需要一个网页 ,将其放置在系统的顶部导航栏中,然后在其下声明一个网页 部分。这个网页部分可以在其下创建一个链接的网页项列表,进而形成下拉菜单上的链接。

以下是执行此操作的步骤:

  1. 在系统的顶部导航栏中创建一个新的网页项:

    <web-item key="jtricks-menu" name="JTricks Menu" section="system.top.navigation.bar" weight="150">
      <description>J Tricks Web site</description>
      <label>J Tricks</label>
      <tooltip>J Tricks Web site</tooltip>
      <link linkId="jtricks-menu">http://www.j-tricks.com</link>
    </web-item>
    

    如你所见,网页部分是system.top.navigation.bar。它可以有一个指向某个位置的链接,在这个例子中是指向 JTricks 网站。需要注意的重要一点是,linkId应该与key相同。在这个例子中,两者都属于jtricks-menu

  2. 在上述网页项下定义一个网页部分:

    <web-section key="jtricks-section" name="JTricks Dropdown" location="jtricks-menu" weight="200"></web-section>
    

    确保位置指向第一个网页项的key,它也是其linkId

  3. 现在,在上述网页部分下添加各种网页项。

    <web-item key="jtricks-item" name="Jtricks Item" section="jtricks-menu/jtricks-section" weight="210">
      <description>J Tricks Tutorials</description>
      <label>J Tricks Tutorials</label>
      <tooltip>Tutorials from J Tricks</tooltip>
      <link linkId="jtricks.link">http://www.j-tricks.com/tutorials</link>
    </web-item>
    

    请注意,该部分指向jtricks-menu/jtricks-section,这类似于一个本地化的部分。在这里,jtricks-menu是第一个网页项的key,而jtricks-section是前一个网页部分的key

  4. 部署插件。

它是如何工作的...

一旦插件被部署,我们可以看到新的网页片段被创建在顶部导航条中。我们有一个网页项,JTricks Menu,在其下,链接列表被分组到一个部分中,正如下面的截图所示:

它是如何工作的...

动态创建网页项

我们现在已经看了不少关于如何创建网页项并将它们放置在 UI 中的不同位置的食谱。但是在所有这些情况下,我们都知道需要哪些链接。那么如何在运行时动态创建这些链接呢?

在本食谱中,我们将看到如何动态创建网页项。

准备开始

按照前面食谱中的讨论,在系统的顶部导航栏中创建一个新的网页项,收藏夹

如何执行...

假设我们想要在系统的顶部导航栏中创建一些链接。我们在之前的食谱中已经看过类似的内容,但那种方法仅在我们预先知道链接的情况下有效。现在让我们考虑一种新的场景:用户在登录和未登录时看到不同的链接集!在这种情况下,链接会根据用户的状态发生变化,因此需要动态创建。

以下是一步一步执行相同操作的过程:

  1. 在系统的顶部导航栏中创建一个收藏夹网页部分。

    <web-item key="favourites-menu" name="Favourites Menu" section="system.top.navigation.bar" weight="900">
      <description>Favourites Menu</description>
      <label>Favourites</label>
      <tooltip>My Favourite Links</tooltip>
      <link linkId="favourites-menu">http://www.j-tricks.com</link>
    </web-item>
    
    <web-section key="favourites-section" name="Favourites Dropdown" location="favourites-menu" weight="200">
    </web-section>
    

    在这里,我们做的正是我们在前一个食谱中看到的内容。一个网页项被创建在顶部导航栏下,其中创建了一个网页部分。

  2. atlassian-plugin.xml中定义一个简单链接****工厂。简单链接工厂定义了一个新的链接工厂,用于动态创建一组链接。它始终挂靠在一个已经声明的网页部分下,在我们的例子中是favourites-section

    <simple-link-factory key="favourites-factory" name="Favourites Link Factory" section="favourites-menu/favourites-section"  i18n-name-key="jtricks.favourites.factory" weight="10" lazy="true" class="com.jtricks.web.links.FavouritesLinkFactory"/>
    
  3. 如你所见,一个简单的链接工厂有一个独特的key,并且指向一个已经存在的location。在我们的例子中,位置是favourites-menu/favourites-section,它在步骤 1中声明。

    最重要的属性是class属性,FavouritesLinkFactory。另外,注意lazy属性被声明为true,表示懒加载。

    创建一个简单的 链接 工厂类。该类应实现SimpleLinkFactory接口,如下所示:

    public class FavouritesLinkFactory implements SimpleLinkFactory {
    
      public List<SimpleLink> getLinks(User user, Map<String, Object> arg1) {
        ...
      }
    
      public void init(SimpleLinkFactoryModuleDescriptor arg0) {
      }
    }
    

    我们只需要实现两个方法,getLinksinitinit方法只有在需要初始化插件中的某些内容时才需要实现。该方法仅在 JIRA 启动时调用一次。

    getLinks方法是我们需要实现的实际方法。

  4. 实现getLinks方法。在此方法中,我们需要返回一个链接集合,这些链接将作为web-item显示在我们之前定义的部分下。

    我们返回的每个链接都是SimpleLink类的实例。SimpleLink对象是我们通常在atlassian-plugin.xml中声明的 web 项的 Java 表示。它具有与labeltitleiconUrlstyleurlaccesskey等属性相同的属性。

    以下是我们示例的方法:

    public List<SimpleLink> getLinks(User user, Map<String, Object> arg1) {
      List<SimpleLink> links = new ArrayList<SimpleLink>();
    
      if (user != null) {
        links.add(new SimpleLinkImpl("id1", "Favourites 1", "My Favourite One", null, null, "http://www.google.com", null));
         links.add(new SimpleLinkImpl("id2", "Favourites 2", "My Favourite Two", null, null, "http://www.j-tricks.com", null));
       } else {
        links.add(new SimpleLinkImpl("id1", "Favourite Link", "My Default Favourite", null, null, "http://www.google.com", null));
      }
      return links;
    }
    

    在这里,我们仅根据用户是否为空来创建不同的链接。如果用户为空,则表示用户未登录。如你所见,每个链接都有之前提到的不同属性。

  5. 打包插件并部署它。

现在,链接应动态创建。

如何操作...

插件部署后,我们可以看到新的 web 片段已在顶部导航栏中创建。如果用户未登录,将显示默认链接的收藏夹菜单,如下图所示:

如何操作...

一旦用户登录,他/她将看到一组不同的链接,如getLinks方法所示。

如何操作...

同样的思路可以用来根据不同的标准创建动态链接,当然,也可以在 UI 中的不同位置使用。

在查看问题屏幕中添加新标签

我们已经看到如何通过在不同位置添加新部分和链接来增强 UI。在这个实例中,我们将看到如何在查看问题页面下添加一个新的标签面板,类似于现有的面板,例如评论、变更历史记录等。

准备工作

使用 Atlassian 插件 SDK 创建一个新的插件骨架。

如何操作...

查看问题页面添加新标签面板可以通过问题 标签面板插件模块来完成。以下是创建一个新问题标签面板的步骤,该面板显示一些静态文本,并向登录用户问候。

  1. atlassian-plugin.xml中定义Issue Tab Panel

    <issue-tabpanel key="jtricks-issue-tabpanel" i18n-name-key="issuetabpanel.jtricks.name" name="Issue Tab Panel" class="com.jtricks.JTricksIssueTabPanel">
      <description>A sample Issue Tab Panel</description>
      <label>JTricks Panel</label>
      <resource type="velocity" name="view" location="templates/issue/issue-panel.vm" />
      <order>100</order>
      <sortable>true</sortable>
    </issue-tabpanel>
    

    在这里,插件模块具有唯一的key,并应定义实现标签面板的class。它还包含一个元素列表,如下所示:

    1. description:标签面板的描述

    2. label:面板的人类可读标签

    3. resource:定义渲染标签面板视图的 Velocity 模板

    4. order:定义面板在查看问题页面上出现的顺序

    5. sortable:定义面板内容是否可排序。例如,排序评论或更改历史元素。

  2. 实现Issue Tab Panel类。

    类应扩展AbstractIssueTabPanel类,而该类又实现IssueTabPanel接口。我们需要实现showPanelgetActions方法。

    1. 实现shownPanel方法,如果面板可以显示给用户,则返回true。此方法可以有复杂的逻辑来检查用户是否可以看到选项卡,但在我们的示例中,我们只返回true

      public boolean showPanel(Issue issue, User remoteUser) {
        return true;
      }
      
    2. 实现需要在getActions方法中返回的IssueAction类。在Action类中,我们填充 Velocity 上下文以渲染视图,并返回执行时间以便于排序,如果sortable = true

      在这个示例中,让我们创建一个单独的Action类,如下所示:

      public class JTricksAction extends AbstractIssueAction{
        private final JiraAuthenticationContext authenticationContext;
      
        public JTricksAction(IssueTabPanelModuleDescriptor 
        descriptor, JiraAuthenticationContext 
        authenticationContext) {
          super(descriptor);
          this.authenticationContext = authenticationContext;
        }
      
        @Override
        public Date getTimePerformed() {
          return new Date();
        }
      
        @Override
        protected void populateVelocityParams(Map params) {	           params.put("user", this.authenticationContext.getUser().getFullName());
        }
      }
      

      如你所见,操作类必须扩展AbstractIssueAction类,而该类又实现IssueAction接口。

      getTimePerformed方法中,它只是返回当前日期。populateVelocityParams是一个重要方法,在其中填充 Velocity 上下文。在我们的示例中,我们只是将当前用户的全名包含在user键中。

    3. Tab Panel类中实现getActions方法,以返回IssueActions列表。在我们的示例中,我们只返回包含新JTricksAction的列表。

      public List getActions(Issue issue, User remoteUser) {
        List<JTricksAction> panelActions = new ArrayList<JTricksAction>();
        panelActions.add(new JTricksAction(descriptor, authenticationContext));	  
        return panelActions;
      }
      

      这里,descriptor是超类的实例变量。我们在这里做的只是创建一个Action类的实例并返回这样的操作列表。

  3. 在之前指定的位置创建视图模板。嘿 $user,示例问题选项卡面板!就是我们需要的,用户信息在Action类中填充到上下文中。

  4. 打包插件并部署它。

它是如何工作的...

一旦插件部署完成,一个新的选项卡面板将出现在查看 问题页面中,如下图所示。

如你所见,那里显示的问候消息是通过 Velocity 上下文及其中的属性填充的。

它是如何工作的...

在浏览项目屏幕中添加新选项卡

在这个示例中,我们将看到如何在浏览 项目屏幕中添加一个新选项卡。

准备就绪

使用 Atlassian 插件 SDK 创建一个新的骨架插件。

如何操作...

创建新项目选项卡面板的步骤如下:

  1. atlassian-plugin.xml中定义Project Tab Panel

    <project-tabpanel key="jtricks-project-panel" i18n-name-key="projectpanels.jtricks.name" name="JTricks Panel" class="com.jtricks.JTricksProjectTabPanel">
      <description>A sample Project Tab Panel</description>
      <label>JTricks Panel</label>
      <order>900</order>
      <resource type="velocity" name="view" location="templates/project/project-panel.vm" />
    </project-tabpanel>
    

    在这里,插件模块具有唯一的key,并应定义实现选项卡面板的class。它还包含一个元素列表,下面将解释:

    1. description:选项卡面板的描述

    2. label:面板的人类可读标签

    3. resource:定义渲染选项卡面板视图的 Velocity 模板

    4. order:定义面板在浏览项目屏幕中出现的顺序。

  2. 实现Project Tab Panel类。

    该类应扩展AbstractProjectTabPanel类,而AbstractProjectTabPanel类又实现ProjectTabPanel接口。我们只需要实现showPanel方法。

    showPanel方法应该返回true,如果面板可以显示给用户。此方法可以包含复杂的逻辑来检查用户是否可以看到选项卡,但在我们的示例中,我们只返回true

    public boolean showPanel(Issue issue, User remoteUser) {
      return true;
    }
    
  3. 在前面指定的位置创建视图模板。我们定义的模板如下:

    Sample Project Tab Panel from <a href="http://www.j-tricks.com">J Tricks</a>
    

    如果我们在此上下文中需要额外的 Velocity 参数,可以通过重写createVelocityParams方法在Project Tab Panel类中填充它。

  4. 打包插件并部署它。

它是如何工作的...

插件部署后,新的选项卡面板将出现在浏览 项目页面,如下所示的屏幕截图所示:

它是如何工作的...

使用碎片创建项目选项卡面板

我们在之前的食谱中已经看到如何创建新的Project Tab Panel。虽然这种方法在大多数情况下有效,但有时我们希望在 JIRA 4.1+中创建漂亮的碎片化视图。在这里,每个项目选项卡面板都有一个按两列组织的碎片列表。我们可以创建碎片并对其排序,以便它们在点击新选项卡面板时以格式化的方式显示。

在本食谱中,我们将看到如何使用碎片创建项目选项卡面板。在开始之前,有几件事值得一提。

  1. 我们需要使用相同的包结构com.atlassian.jira.plugin.projectpanel.impl来创建碎片类,因为我们需要重写其中的受保护方法。

  2. 创建碎片所使用的组件在 OSGI v2.0 插件中不可用,因此我们选择使用 v1.0 插件。

如何实现...

以下是创建碎片化Project Tab Panel的步骤。

  1. atlassian-plugin.xml中添加项目选项卡面板模块。

    <project-tabpanel key="jtricks-project-fragment-panel" i18n-name-key="projectpanels.fragments.jtricks.name" name="JTricks Frag Panel" class="com.atlassian.jira.plugin.projectpanel.impl.JTricksFragProjectTabPanel">	
      <description>A sample Project Tab Panel with fragments</description>
      <label>JTricks Fragments Panel</label>
      <order>910</order>
    </project-tabpanel>
    

    这些属性和元素类似于普通的Project Tab Panel,只不过它没有定义视图 Velocity 资源。这里的 HTML 是通过碎片帮助构建的。

  2. 创建项目面板中所需的碎片。假设我们需要两个碎片,FragmentOneFragmentTwo,作为示例。

    每个碎片必须扩展AbstractFragment类。我们需要为碎片重写三个方法。

    1. getId:它定义了碎片的 ID,这也将是用于渲染此碎片的 Velocity 模板的名称。

    2. getTemplateDirectoryPath:它返回放置 Velocity 模板的路径。

    3. showFragment:它定义了碎片是否对用户可见。

    如果我们需要向 Velocity 上下文传递额外的参数,可以重写第四个方法createVelocityParams。以下是FragmentOne的代码:

    public class FragmentOne extends AbstractFragment{
      protected static final String TEMPLATE_DIRECTORY_PATH = "templates/project/fragments/";
      public FragmentOne(VelocityManager velocityManager, ApplicationProperties applicationProperites, JiraAuthenticationContext jiraAuthenticationContext) {  
        super(velocityManager, applicationProperites, jiraAuthenticationContext);
      }
      public String getId() {
        return "fragmentone";
      }
      public boolean showFragment(BrowseContext ctx) {
        return true;
      }
      @Override
      protected String getTemplateDirectoryPath() {
        return TEMPLATE_DIRECTORY_PATH;
      }
      @Override
      protected Map<String, Object> createVelocityParams(BrowseContext ctx) {
        Map<String, Object> createVelocityParams = super.createVelocityParams(ctx);
        createVelocityParams.put("user", ctx.getUser().getFullName());
        return createVelocityParams;
      }
    }
    

    在这里,Velocity 模板将是 fragmentone.vm,并放置在 templates/project/fragments/ 下。碎片始终显示,但可以修改以包含复杂的逻辑。我们还将一个新的变量 user 添加到上下文中,该变量存储当前用户的全名。请注意,user 变量已经在上下文中,但这里只是作为示例。

    FragmentTwo 将类似于接下来的几行代码:

    public class FragmentTwo extends AbstractFragment {
      protected static final String TEMPLATE_DIRECTORY_PATH = "templates/project/fragments/";
      public FragmentTwo(VelocityManager velocityManager, ApplicationProperties applicationProperites, JiraAuthenticationContext jiraAuthenticationContext) {
        super(velocityManager, applicationProperites, jiraAuthenticationContext);
      }
      public String getId() {
        return "fragmenttwo";
      }
      public boolean showFragment(BrowseContext ctx) {
        return true;
      }
      @Override
      protected String getTemplateDirectoryPath() {
        return TEMPLATE_DIRECTORY_PATH;
      }
    }
    

    在这里,Velocity 模板将是 templates/project/fragments/fragmenttwo.vm。请注意,由于我们不需要上下文中的任何额外参数,因此我们没有重写 createVelocityParams 方法。

  3. 创建基于碎片的项目标签面板类。在我们的示例中,类名为 JTricksFragProjectTabPanel。该类必须继承 AbstractFragmentBasedProjectTabPanel 类。我们需要在此类中实现三个方法:

    1. getLeftColumnFragments:返回一个 ProjectTabPanelFragment 类的列表,构成面板的左侧列。

    2. getRightColumnFragments:返回一个 ProjectTabPanelFragment 类的列表,构成面板的右侧列。

    3. showPanel:确定面板是否可以显示。

    该类将如下所示:

    public class JTricksFragProjectTabPanel extends AbstractFragmentBasedProjectTabPanel {
      private final FragmentOne fragmentOne;
      private final FragmentTwo fragmentTwo;
      public JTricksFragProjectTabPanel(VelocityManager velocityManager, ApplicationProperties applicationProperites, JiraAuthenticationContext jiraAuthenticationContext) {
        this.fragmentOne = new FragmentOne(velocityManager, applicationProperites, jiraAuthenticationContext);
        this.fragmentTwo = new FragmentTwo(velocityManager, applicationProperites, jiraAuthenticationContext);
      }
    
      @Override
      protected List<ProjectTabPanelFragment> getLeftColumnFragments(BrowseContext ctx) {
        final List<ProjectTabPanelFragment> frags = new ArrayList<ProjectTabPanelFragment>();
        frags.add(fragmentOne);
        return frags;
      }
      @Override
      protected List<ProjectTabPanelFragment> getRightColumnFragments(BrowseContext ctx) {
        final List<ProjectTabPanelFragment> frags = new ArrayList<ProjectTabPanelFragment>();
        frags.add(fragmentTwo);
        return frags;
      }
    
      public boolean showPanel(BrowseContext ctx) {
        return true;
      }
    }
    

    在这里,我们仅构造碎片对象并将它们返回到适当的列列表中。

  4. 为碎片创建 Velocity 模板。

    在我们的示例中,fragmentone.vm 如下所示:

    <div class="mod-header">
      <h3>Fragment 1</h3>
    </div>
    <div class="mod-content">
      <ul class="item-details">
        <li>Welcome, $user!</li>
        <li>This is fragment 1.</li>
      </ul>
    </div>
    

    注意 $user 的使用,它是在 FragmentOne 类的 Velocity 上下文中填充的。此外,使用了各种 div 元素来确保 UI 的一致性。

    类似地,fragmenttwo.vm 如下所示:

    <div class="mod-header">
      <h3>Fragment 2</h3>
    </div>
    <div class="mod-content">
      <ul class="item-details">
        <li>This is fragment 2!!</li>
      </ul>
    </div>
    

    唯一的不同之处是这里不使用 velocity 变量。

  5. 打包插件并部署。

如何操作...

一旦插件部署完成,浏览 项目 页面将出现一个基于碎片的新标签面板,如下图所示:

如何操作...

在浏览版本屏幕中添加新标签

在本教程中,我们将展示如何在 浏览 版本 屏幕中添加新标签。该屏幕显示 JIRA 中特定版本的详细信息。

准备工作

使用 Atlassian Plugin SDK 创建一个新的骨架插件。

如何操作...

以下是创建新版本标签面板的步骤。它与创建新项目标签面板非常相似,除了涉及的文件和关键字的明显变化。

  1. atlassian-plugin.xml 中定义 Version Tab Panel

    <version-tabpanel key="jtricks-version-panel" i18n-name-key="versionpanels.jtricks.name" name="jtricks Version Panel" class="com.jtricks.JTricksVersionTabPanel">
      <description>A sample Version Tab Panel</description>
      <label>JTricks Panel</label>
      <order>900</order>
    
      <resource type="velocity" name="view" location="templates/version/version-panel.vm" />
    </version-tabpanel>
    

    在这里,插件模块具有唯一的 key,并且应定义实现标签面板的 class。它还具有一个元素列表,如下所述:

    1. description:标签面板的描述

    2. label:面板的可读标签

    3. resource:定义渲染标签面板视图的 Velocity 模板

    4. order:定义面板在浏览版本屏幕上显示的顺序。

  2. 实现 Version Tab Panel 类。

    类应扩展GenericTabPanel类,该类又实现TabPanel接口。我们只需要实现showPanelcreateVelocityParams方法,后者只有在需要将额外变量添加到 velocity 上下文时才需要实现。

    如果面板可以显示给用户,showPanel方法应该返回 true。该方法可以包含复杂的逻辑来检查用户是否能看到标签,但在我们的示例中,我们只是返回 true。

    public boolean showPanel(Issue issue, User remoteUser) {
      return true;
    }
    
    Let us just override the createVelocityParams method to add a new variable, user to the velocity context.
    @Override
    protected Map<String, Object> createVelocityParams(BrowseVersionContext context) {
      Map<String, Object> createVelocityParams = super.createVelocityParams(context);
      createVelocityParams.put("user", context.getUser().getFullName());
      return createVelocityParams;
    }
    

    该变量现在将在我们使用的视图模板中可用。

  3. 在插件描述符中指定的位置创建视图模板。让我们只创建一个简单的模板,向当前用户问好,如下所示:

    欢迎 $user,这是你的新版本标签!

    请注意,我们已经使用了在前一步中填充的$user变量。

  4. 打包插件并部署它。

它是如何工作的...

一旦插件部署,新标签面板将在浏览 版本页面上显示,如下图所示:

它是如何工作的...

在浏览组件屏幕中添加新的标签

在本食谱中,我们将看到如何在浏览 组件屏幕中添加一个新的标签。此屏幕显示 JIRA 中特定组件的详细信息,添加新标签的过程与添加新版本标签或项目标签面板非常相似。

准备工作

使用Atlassian 插件 SDK创建一个新的骨架插件。

如何操作...

以下是创建新组件标签面板的步骤。

  1. atlassian-plugin.xml中定义Component Tab Panel

    <component-tabpanel key="jtricks-component-panel" i18n-name-key="componentpanels.jtricks.name" name="jtricks Component Panel" class="com.jtricks.JTricksComponentTabPanel">
    
      <description>A sample Component Tab Panel</description>
      <label>JTricks Panel</label>
      <order>900</order>
      <resource type="velocity" name="view" location="templates/component/component-panel.vm" />
    </component-tabpanel>
    

    与版本标签面板类似,组件标签面板也有一个唯一的键,并应定义实现标签面板的class。它还具有元素列表,如下所述:

    1. description:标签面板的描述

    2. label:面板的可读标签

    3. resource:定义渲染标签面板视图的 velocity 模板

    4. order:定义面板在浏览组件屏幕中出现的顺序。

  2. 实现Component Tab Panel类。

    类应扩展GenericTabPanel类,该类又实现ComponentTabPanel接口。我们只需要实现showPanel方法和createVelocityParams方法,后者只有在需要将额外变量添加到 velocity 上下文时才需要实现。

    如果面板可以显示给用户,showPanel方法应该返回 true。该方法可以包含复杂的逻辑来检查用户是否能看到标签,但在我们的示例中,我们只是返回 true。

    public boolean showPanel(Issue issue, User remoteUser) {
      return true;
    }
    

    如同之前的步骤,我们将重写createVelocityParams方法,将一个新的变量user添加到 velocity 上下文中。

    @Override
    protected Map<String, Object> createVelocityParams(BrowseVersionContext context) {
      Map<String, Object> createVelocityParams = super.createVelocityParams(context);
      createVelocityParams.put("user", context.getUser().getFullName());
      return createVelocityParams;
    }
    

    该变量现在将在我们使用的视图模板中可用。

  3. 在插件描述符中指定的位置创建视图模板。让我们创建一个简单的模板,如前面所示,如下所示:

    欢迎 $user,这是你的新组件标签!

    请注意,我们使用了 $user 变量,这是我们在前一步填充的。

  4. 打包插件并部署它。

它是如何工作的...

一旦插件被部署,在浏览组件页面上将出现一个新的标签面板,如下图所示:

它是如何工作的...

扩展 Webwork 动作以添加 UI 元素

在第二章中,我们已经看到如何扩展 Webwork 动作。在这个实例中,让我们基于此,看看如何为现有的 JIRA 表单添加更多 UI 元素。

准备工作

使用 Atlassian 插件 SDK 创建一个骨架插件。

如何做...

我们可以考虑一个简单的例子来解释这个过程。JIRA 中的克隆问题操作会创建一个原始问题的副本,几乎所有字段的值都会被复制过去,除了少数几个字段,比如问题键、创建日期、更新日期、估算、投票数等。

如果我们还想复制问题上的投票数量呢?假设我们想在克隆问题表单上添加一个复选框,让用户决定是否复制投票。如果用户勾选了该复选框,则投票会被复制;如果没有勾选,则克隆的任务将默认创建为 0 票,这与 JIRA 中的默认行为一致。这个例子将大致说明如何在 JIRA 表单中添加新的 UI 元素,并在操作类中使用它们。

以下是实现我们示例的逐步过程:

  1. 通过在atlassian-plugin.xml中为CloneIssue创建条目,覆盖 JIRA 的 Web 动作。

    <webwork1 key="clone-issue" name="Clone Issue with new UI elements" class="java.lang.Object">
      <description>Sample Webwork action with extended UI elements</description>
      <actions>
        <action name="com.jtricks.web.action.ExtendedCloneIssueDetails" alias="CloneIssueDetails">
           <view name="input">/secure/views/extended-cloneissue-start.jsp</view>
          <view name="error">/secure/views/extended-cloneissue-start.jsp</view>
        </action>
      </actions>
    </webwork1>
    

    如我们在第二章中所见,理解 插件 框架,动作的别名与Clone Issue动作保持一致。我们选择了一个自定义类ExtendedCloneIssueDetails,它继承了 JIRA 动作类CloneIssueDetails

    这里,我们还使用了原始 JSP 的副本,命名为extended-cloneissue-start.jsp,仅仅是为了追踪修改过的文件。完全可以直接修改 JIRA 提供的cloneissue-start.jsp

  2. 创建新的动作类,继承原始动作类。虽然也可以创建一个全新的动作类,但继承原始动作类会更简单,因为这仅仅需要我们添加额外的部分。

    public class ExtendedCloneIssueDetails extends CloneIssueDetails {
    
      public ExtendedCloneIssueDetails(ApplicationProperties applicationProperties, PermissionManager permissionManager, IssueLinkManager issueLinkManager, IssueLinkTypeManager issueLinkTypeManager, SubTaskManager subTaskManager, AttachmentManager attachmentManager, FieldManager fieldManager, IssueCreationHelperBean issueCreationHelperBean, IssueFactory issueFactory, IssueService issueService) {
      super(applicationProperties, permissionManager, issueLinkManager, issueLinkTypeManager, subTaskManager, attachmentManager, fieldManager, issueCreationHelperBean, issueFactory, issueService);}
      ...
    }
    
  3. 声明一个变量用于我们将要添加的克隆问题表单的新字段。变量名将与 UI 元素的名称相同。

    我们声明的变量应该与 UI 元素的类型匹配。例如,复选框将有一个 Java Boolean 类型的变量。

    private boolean cloneVotes = false;
    

    不同的 UI 元素被映射到不同的 Java 类型,例如文本框映射到 Java String,数字框映射到 Long,等等。

  4. 为新字段创建 getter 和 setter。

    public boolean isCloneVotes() {
      return cloneVotes;
    }
    
    public void setCloneVotes(boolean cloneVotes) {
      this.cloneVotes = cloneVotes;
    }
    

    这些 getter/setter 方法将用于在 JSP 中获取和传递值。

  5. 将新的 UI 元素添加到 JSP 中。在我们的例子中,JSP 是 extended-cloneissue-start.jsp,它是 cloneissue-start.jsp 的一个副本。我们添加的 UI 元素应该遵循我们使用的 JIRA 版本中的模板规则。这一点很重要,因为不同版本的 UI 会有所变化。

    在 JIRA 4.3 中,新的复选框可以按如下方式添加:

    <page:applyDecorator name="auifieldgroup">
      <aui:checkbox label="'Clone Votes?'" name="'cloneVotes'" fieldValue="'true'" theme="'aui'">
        <aui:param name="'description'">
          <ww:text name="'Clone the votes on the original issue?'"/>
        </aui:param>
      </aui:checkbox>
    </page:applyDecorator>
    

    请注意,复选框字段的 name 与 Action 类中的类变量名称相同。其余的代码围绕着装饰器的使用、要传递的属性和元素等进行。

  6. 在表单提交时,在 action 类中捕获复选框的值。该值可以通过 getter 方法在类中获取。我们现在可以根据复选框的值进行所有需要的操作。

    在这种情况下,如果选中 cloneVotes 复选框,投票字段应该从原始问题复制到克隆问题。正如我们在前一章中看到的,在克隆时丢弃字段值的操作中,我们可以重写 setFields 方法来实现这一点。

    @Override
    protected void setFields() throws FieldLayoutStorageException {
      super.setFields();
      if (isCloneVotes()) {
        getIssueObject().setVotes(getOriginalIssue().getVotes());
      }
    }
    

    这里的关键是复选框的值会传递到后台的 action 类,我们使用这个值来决定是否复制投票的值。

  7. 打包插件,部署并查看其实际效果。别忘了将修改后的 JSP 文件复制到 /secure/views 文件夹。

一个 action 只能被重写一次。需要小心,不要在另一个插件(可能是第三方插件)中再次重写它,因为只有一个会被选中。

它是如何工作的...

一旦插件部署完成并且 JSP 文件被复制到正确的位置,我们可以在克隆问题时看到修改后的 UI。表单将新增 克隆 投票? 字段,如下图所示:

它是如何工作的...

请注意,我们正在克隆的问题上有一个投票。如果勾选了 cloneVotes 字段,克隆问题将会带有投票,如下图所示:

它是如何工作的...

使用类似的方法可以将任何新字段添加到 JIRA 表单中,并根据需要在 action 类中使用这些字段。你可以通过查看 WEB-INF/tld/ 文件夹中的 webwork.tld 文件,找到关于各种元素及其属性的更多信息。

参见

  • 扩展 JIRA 中的 Webwork 动作

在问题上显示动态通知/警告

JIRA 有一个有趣的功能,公告横幅(Announcement Banner),可以通过 JIRA 本身向用户社区发布公告。但有时,这个功能并不足以满足所有用户的需求。JIRA 的高级用户有时希望根据问题的一些属性,在查看问题时看到警告或通知。

在这个教程中,我们将展示如何根据问题是否有子任务,在问题上添加警告或错误信息!

准备工作

使用 Atlassian Plugin SDK 创建一个 Skeleton 插件。这里与前一个配方一样,核心逻辑是扩展 JIRA 操作并修改现有的 JSP 文件。

如何实现...

以下是根据标准问题类型的子任务数量显示警告/错误的步骤。

  1. 如前所述,通过在atlassian-plugin.xml中添加一个 webwork 模块来扩展 JIRA 操作(在这种情况下是视图操作)。

    <webwork1 key="view-issue" name="View Issue with warning" class="java.lang.Object">
      <description>View Issue Screen with Warnings</description>
        <actions>
          <action name="com.jtricks.web.action.ExtendedViewIssue" alias="ViewIssue">
            <view name="success">/secure/views/issue/extended-viewissue.jsp</view>
            <view name="issuenotfound">/secure/views/issuenotfound.jsp</view>
            <view name="permissionviolation">/secure/views/permissionviolation.jsp</view>
            <command name="moveIssueLink" alias="MoveIssueLink">
              <view name="error">/secure/views/issue/viewissue.jsp</view>
            </command>
          </action>
        </actions>
    </webwork1>
    
  2. 重写ViewIssue操作类为ExtendedViewIssue,并添加一个公共方法来检查问题是否有子任务。确保该方法是公共的,以便可以从 JSP 中调用。

    public boolean hasNoSubtasks(){
      return !getIssueObject().isSubTask() && getIssueObject().getSubTaskObjects().isEmpty();
    }
    
  3. 它仅检查问题是否是子任务,如果是标准的issuetype并且没有自己的子任务,则返回true

  4. 修改extended-viewissue.jsp文件,在顶部添加警告,如果问题没有子任务。我们在这里添加了一个条件来检查公共方法是否返回true,如果是,则添加警告,如下所示:

    <ww:if test="/hasNoSubtasks() == true">
      <div class="aui-message warning">
        <span class="aui-icon icon-warning"></span>
        The Issue has no Subtasks!  - WARNING
      </div>
    </ww:if>
    
  5. 如你所见,我们已经使用了 JIRA 样式来添加警告图标和警告 div 容器。

  6. 打包插件并部署,查看它的实际效果。

它是如何工作的...

一旦插件部署完成,如果一个标准问题没有子任务,用户将在下方看到一个警告,如果警告代码紧接着出现在摘要字段之后。

它是如何工作的...

仅通过修改 CSS 来添加错误样式,消息将如图所示:

<ww:if test="/hasNoSubtasks() == true">
  <div class="aui-message error">
    <span class="aui-icon icon-error"></span>
    The Issue has no Subtasks! - ERROR
  </div>
</ww:if>

它是如何工作的...

另请参见

  • 扩展 JIRA 中的 webwork 操作

在视图问题页面中重新排序问题操作

在上一章中,我们已经了解了如何创建新的问题操作。所有现有的 JIRA 问题操作都有一个预定义的顺序。目前,在 JIRA 中,操作的排序如下所示:

重新排序视图问题页面中的操作

在这个配方中,我们将展示如何在不做任何编码的情况下重新排序这些操作!例如,假设我们想把删除选项移到列表的最前面,然后将子任务操作上移!

如何实现...

以下是重新排序问题操作的逐步过程:

  1. 转到位于WEB-INF/classes文件夹下的system-issueoperations-plugin.xml。这是定义所有问题操作的文件。

  2. 修改相关插件模块中的weight属性来排序它们。

    weight是定义 JIRA 网页片段顺序的属性。JIRA 4.1.x 之后,问题操作作为网页片段存储,因此使用 weight 进行重新排序。

    在 JIRA 4.1 之前,问题操作是通过问题操作插件模块定义的,而不是web-items。在这些模块中,使用order属性来定义排序,这与当前的weight属性相当。

    降低weight值,项目将排在最前面。

  3. 保存文件并重启 JIRA。

它是如何工作的...

在我们的示例中,我们希望“删除”操作排在最前面。由于 10 是默认的最低权重值,如果我们为delete网页部分(即operations-delete)赋予权重值 5,它将在列表中排在最前面,如下所示。类似地,我们可以重新排序其他操作,就像我们对子任务操作所做的那样。

它是如何工作的...

参见

  • 添加 问题 操作

重新排序查看问题页面中的字段

要满足一个大用户社区中的所有人总是很难的,这也是 JIRA 查看问题页面的情况。虽然一些人喜欢它,另一些人认为有简单的改进可以提高客户满意度。

其中一件事是查看问题页面的布局。虽然在代码层面它被整洁地组织,但它们出现的顺序在很多情况下似乎是改变的有力候选项。

例如,在查看问题页面中,问题的摘要后面是标准问题字段,如状态、优先级、版本、组件等。接着是自定义字段,最后是问题描述。这样有时会有些麻烦,例如在描述是最重要字段的情况下。

当你有一个大型自定义字段时,以下是查看问题页面的样式:

重新排序查看问题页面中的字段

如你所见,Test Free Text字段的权重大,而description字段在页面上完全没有出现。在这个示例中,我们将展示如何重新排序一些字段。

如何操作...

正如我们在前一个示例中看到的,查看问题操作使用了/secure/views/issue/viewissue.jsp。如果我们仔细查看 JSP,会发现它通过将不同字段部分放入不同的 JSP 中来进行整洁的安排。例如,自定义字段、描述、附件、链接、子任务等,都有各自专门的 JSP,并按对大多数人有效的顺序排列。

如果我们希望description成为查看问题页面的第一个字段,我们可以通过将负责渲染描述的 JSP(即issue_descriptiontable.jsp)上移到更靠前的位置来实现。

页面将如下所示:

如何操作...

我们甚至可以将自定义字段包装在一个单独的div中,并使用适当的 CSS 类,如下图所示。确保不要重复值。

<div id="details-module" class="module toggle-wrap">
  <div class="mod-header">|
     <h3 class="toggle-title">Custom Fields</h3>
   </div>
  <div class="mod-content">
    <jsp:include page="/includes/panels/issue/view_customfields.jsp" />
  </div>
</div>

如何操作...

类似地,我们也可以重新排序 JIRA 的查看 问题页面上的其他 UI 组件!

第九章:远程访问 JIRA

本章内容包括:

  • 创建一个 SOAP 客户端

  • 通过 SOAP 创建问题

  • 使用自定义字段和 SOAP

  • 附件和 SOAP

  • 通过 SOAP 进行工作日志和时间跟踪

  • 通过 SOAP 评论问题

  • 通过 SOAP 进行用户和组管理

  • 使用 SOAP 在工作流中推进问题

  • 通过 SOAP 管理版本

  • SOAP API 中的管理方法

  • 在 JIRA 中部署 SOAP 服务

  • 在 JIRA 中部署 XML-RPC 服务

  • 编写 Java XML-RPC 客户端

  • 将服务和数据实体暴露为 REST API

  • 为 REST API 编写 Java 客户端

介绍

我们在前面的章节中看到了多种方式来增强 JIRA 的功能,但如何从另一个应用程序与 JIRA 通信呢?有哪些不同的方法可以将第三方应用程序集成到 JIRA 中?或者换句话说,JIRA 是如何将其功能暴露给外部世界的?

JIRA 通过 RESTSOAPXML/RPC 接口暴露其功能。通过这些接口暴露的 JIRA 完整功能有限,但 JIRA 也允许我们扩展这些接口。在本章中,我们将学习如何通过这些接口与 JIRA 通信,并通过开发插件将更多方法添加到这些接口中。本章的重点是 SOAP,同时也会提供其他接口的示例。这些接口的核心原理是相同的。

SOAP 中有很多方法,大多数方法可以从 API 中很好地理解,API 地址为 docs.atlassian.com/software/jira/docs/api/rpc-jira-plugin/latest/com/atlassian/jira/rpc/soap/JiraSoapService.html。但有一些方法需要更好的理解,这些方法将在本章中重点讲解,其他的留给读者自行研究。

SOAP 通常是最受欢迎的远程访问方法,但 Atlassian 正在慢慢转向 REST 作为首选模式。有关这些接口的更详细解释可以在以下网址找到:confluence.atlassian.com/display/JIRADEV/JIRA+RPC+Services

创建一个 SOAP 客户端

如前所述,SOAP 目前是 JIRA 首选的远程访问模式,尽管 Atlassian 正在慢慢转向 REST。与 REST 或 XML/RPC 相比,SOAP 提供了最多的方法,可能在我们周围的插件中使用最广泛。在本食谱中,我们将从基础开始,看看如何编写一个简单的 SOAP 客户端。

准备工作

安装 Maven2 并配置 Java 开发环境。确保在 JIRA 中启用了 RPC 插件,并且在 Administration | Global Settings | General Configuration 中启用了 Accept remote API calls 选项。

如何操作…

以下是创建 JIRA SOAP 客户端的步骤:

  1. 从 Atlassian 公共仓库下载最新的示例 SOAP 客户端分发包,链接为:svn.atlassian.com/svn/public/atlassian/rpc-jira-plugin/tags/。它包含一个配置使用 Apache Axis 的 Maven 2 项目,以及一个示例 Java SOAP 客户端,该客户端在 jira.atlassian.com 创建测试问题。

  2. 修改 pom.xml 中的 jira.soapclient.jiraurl 属性,指向你想要连接的 JIRA 实例。默认情况下,它指向 jira.atlassian.com

  3. 下载你想要连接的实例的 WSDL 文件。你可以在 /src/main/wsdl 位置找到 WSDL 文件。如果没有该文件,或者你想重新下载 WSDL,可以运行以下命令:

    mvn -Pfetch-wsdl -Djira.soapclient.jiraurl=http://{your_jira_instance}/
    
  4. 这将从配置的 JIRA 实例(如步骤 2所示)下载 WSDL 到 /src/main/wsdl/。跳过 jira.soapclient.jiraurl 属性以下载 Atlassian JIRA 的 WSDL。

  5. 创建客户端 JAR。我们可以通过运行以下命令从 WSDL 生成源代码并创建 SOAP 客户端:

    mvn -Pbuildclient
    
  6. 这将生成一个 JAR 文件,包含所有必需的类。还会创建一个包含依赖项(例如 Axis)的第二个 JAR 文件。如果你在没有配置 Axis 和其他依赖项的环境中执行,这个 JAR 文件会非常有用。

  7. 编写客户端程序。让我们按照本教程中的最简单方法进行,也就是在 Eclipse 中创建一个简单的独立 Java 类。首先,通过运行以下命令创建一个 Eclipse 项目:

    mvn eclipse:eclipse
    

    注意

    你也可以尝试其他 IDE,或者直接从命令提示符运行,选择你方便的方式。确保将步骤 4中创建的客户端 JAR 添加到类路径中。现在可以编写一个简单的程序,直接登录到我们的 JIRA 实例。从现在开始,这只是另一种 Web 服务调用,按照接下来的步骤进行。

  8. 创建独立的 Java 类。

  9. 获取 SOAP 服务定位器:

    JiraSoapServiceServiceLocator jiraSoapServiceLocator = new JiraSoapServiceServiceLocator();
    
  10. 从定位器获取 SOAP 服务实例,方法是传递你的 JIRA 实例的 URL:

    JiraSoapService jiraSoapService = jiraSoapServiceLocator.getJirasoapserviceV2(new URL(your_url));
    
  11. 开始使用 SOAP 服务实例访问方法。例如,登录操作可以如下进行:

    String token = jiraSoapService.login(your_username, your_password);
    

    这里获取的令牌将用于所有其他操作,而不需要每次登录。你可以将令牌视为所有其他操作的第一个参数。

  12. 到此为止,我们的 SOAP 客户端已经准备好。让我们尝试使用问题的关键字获取一个问题,并打印它的关键字和 ID,证明这个功能是有效的!

    RemoteIssue issue = jiraSoapService.getIssue(authToken, ISSUE_KEY);
    System.out.println("Retrieved Issue:"+issue.getKey()+" with Id:"+issue.getId());
    

    你将看到输出中打印出问题的关键字和 ID。

希望这能给你一个大致的了解,帮助你开始使用第一个 SOAP 客户端!你可以用 SOAP 客户端做更多事情,我们将在接下来的教程中看到其中的一些。

通过 SOAP 创建问题

在前面的教程中,我们已经看到如何创建一个 SOAP 客户端。我们还看到了如何使用客户端连接到 JIRA 实例并执行操作,以“浏览问题”为例。在本教程中,我们将看到如何使用 SOAP API 创建一个问题。

准备工作

创建一个 JIRA SOAP 客户端,如前面的教程中所提到的。

如何操作……

以下是创建一个已填充标准字段的问题的步骤:

  1. 如前面的教程所提到的,获取 JIRA SOAP 服务存根并登录到系统:

    JiraSoapServiceServiceLocator jiraSoapServiceLocator = new JiraSoapServiceServiceLocator();
    JiraSoapService jiraSoapService = jiraSoapServiceLocator.getJirasoapserviceV2(new URL(your_url));
    String authToken = jiraSoapService.login(userName, password);
    
  2. 创建 RemoteIssue 的实例:

    RemoteIssue issue = new RemoteIssue();
    
  3. 根据需要填充 RemoteIssue 上的标准字段:

    issue.setProject(PROJECT_KEY);
    issue.setType(ISSUE_TYPE_ID);
    issue.setSummary("Test Issue via my tutorial");
    issue.setPriority(PRIORITY_ID);
    issue.setDuedate(Calendar.getInstance());
    issue.setAssignee("");
    

    确保 PROJECT_KEYISSUE_TYPE_IDPRIORITY_ID 等都是你 JIRA 实例中的有效值。ISSUE_TYPE_IDPRIORITY_ID 是 ID,而不是问题类型和优先级的名称。

  4. 在问题上设置组件。一个问题可以有多个组件,因此我们需要设置一个 RemoteComponent 对象的数组,如以下代码块所示:

    RemoteComponent component1 = new RemoteComponent();
    component1.setId(COMPONENT_ID1);
    RemoteComponent component2 = new RemoteComponent();
    component2.setId(COMPONENT_ID2);
    issue.setComponents(new RemoteComponent[] { component1, component2 });
    

    我们可以拥有任意数量的组件,只要 id 实例是我们在创建问题时所在项目中的有效组件 ID。这里,id 是你在浏览组件时找到的唯一 ID,如下图所示:

    如何操作...

  5. 设置 Fix for VersionsAffected Versions,类似于我们设置组件的方式:

    RemoteVersion version = new RemoteVersion();
    version.setId(VERSION_ID);
    RemoteVersion[] remoteVersions = new RemoteVersion[] { version };
    issue.setFixVersions(remoteVersions);
    

    再次提醒,VERSION_ID 是版本的唯一标识符,可以在浏览版本时找到,就像我们在处理组件时所做的那样。

  6. 在 SOAP 客户端上调用创建问题操作,传递身份验证令牌和我们构建的 RemoteIssue 对象。

    RemoteIssue createdIssue = jiraSoapService.createIssue(authToken, issue);
    
  7. 问题现在应该已经创建,并且其细节(如 ID)可以通过返回的 RemoteIssue 对象获取,可以按如下方式打印:

    System.out.println("\tSuccessfully created issue " + createdIssue.getKey() + " with ID:" + createdIssue.getId());
    

它是如何工作的……

这只是一个经典示例,演示了如何使用 Axis2 从 Java 应用程序调用 Web 服务。一旦按照前面的说明编写了 Java 客户端,我们就可以运行它,问题将在我们在客户端中引用的实例中创建。

以下是一个在 TEST 项目中创建的问题的截图,链接为 jira.atlassian.com

它是如何工作的...

如你所见,问题已经填充了我们在 RemoteIssue 对象上设置的所有字段。

使用自定义字段和 SOAP

我们已经了解了如何创建一个带有标准字段的问题。在本教程中,我们将处理自定义字段——创建更新读取它们的值。

准备工作

如前面的教程所示,创建一个 JIRA SOAP 客户端。

如何操作……

如前所述,在本教程中,我们将分别讨论自定义字段值的创建、更新和浏览。

使用自定义字段值创建问题

创建带有自定义字段的问题与创建带有组件或版本的问题非常相似。所有自定义字段都通过一个方法setCustomFieldValues设置,该方法接受一个RemoteCustomFieldValue对象的数组。

以下步骤解释了操作过程:

  1. 确定需要在问题上设置的自定义字段并找到它们的 ID。自定义字段的 ID 形式为customfield_[id],其中[id]是自定义字段的数据库 ID。可以从数据库中确定该 ID,或者通过在管理界面中编辑自定义字段,复制其 ID(从 URL 中提取)来获取 ID,如下图所示:创建带有自定义字段值的问题

  2. 为每个识别出的自定义字段创建一个RemoteCustomFieldValueRemoteCustomFieldValue可以按如下方式创建:

    RemoteCustomFieldValue customFieldValue = new RemoteCustomFieldValue(CUSTOM_FIELD_KEY, "", new String[] { CUSTOM_FIELD_VALUE1, CUSTOM_FIELD_VALUE2 });
    

    我们传递的值——CUSTOM_FIELD_VALUE1CUSTOM_FIELD_VALUE2,依此类推——应该是该字段的有效值,否则会导致服务器上的验证错误。对于单值自定义字段,数组中将仅包含一个值。第二个属性接受一个parentKey值,仅用于多维字段,如级联选择列表。对于单值和多值字段(如选择列表、多选等),parentKey将是一个空字符串。

    例如,RemoteCustomFieldValue用于构建级联选择如下:

    RemoteCustomFieldValue customFieldValue = new RemoteCustomFieldValue(CUSTOM_FIELD_KEY_2, "PARENT_KEY", new String[] { CUSTOM_FIELD_VALUE_2 });
    

    父键将用于构建自定义字段的完整键。例如,一个级联选择将具有完整的自定义字段键,如customfield_10061:1,其中customfield_10061是父字段的键,:1表示第一个子字段。事实上,以下两个表示的是同一件事:

    RemoteCustomFieldValue customFieldValue = new RemoteCustomFieldValue("customfield_10061", "1", new String[] { "Some Val" });
    
    RemoteCustomFieldValue customFieldValue = new RemoteCustomFieldValue("customfield_10061:1", null, new String[] { "Some Val" });
    
  3. 设置所有自定义字段值的数组到问题中。

    RemoteCustomFieldValue[] customFieldValues = new RemoteCustomFieldValue[] { customFieldValue1, customFieldValue2 };
    issue.setCustomFieldValues(customFieldValues);
    
  4. 像往常一样创建问题:

    RemoteIssue createdIssue = jiraSoapService.createIssue(authToken, issue);
    

更新问题上的自定义字段

更新自定义字段非常相似。然而,在updateIssue方法中,它接受一个RemoteFieldValue的数组,这些字段可以是标准字段或自定义字段。在自定义字段的情况下,我们应设置自定义字段的 ID(如果是多维字段,则为完整 ID,因为RemoteFieldValue不接受父键!)以及字符串值的数组,如下所示:

RemoteFieldValue[] actionParams = new RemoteFieldValue[] { new RemoteFieldValue(CUSTOM_FIELD_KEY, new String[] { CUSTOM_FIELD_VALUE }) };

现在可以按如下方式更新问题:

RemoteIssue updatedIssue = jiraSoapService.updateIssue(authToken, ISSUE_KEY, actionParams);

这里,ISSUE_KEY是要更新的问题的键。

请注意,updateIssue()方法也用于更新标准字段,但唯一的区别是,RemoteFieldValue中使用的key将是标准字段的键。标准字段的键可以从IssueFieldConstants类中找到。

浏览问题上的自定义字段

可以通过在RemoteIssue上使用getCustomFieldValues方法来检索问题上的自定义字段。然后可以按如下方式打印:

RemoteCustomFieldValue[] cfValues = issue.getCustomFieldValues();
for (RemoteCustomFieldValue remoteCustomFieldValue : cfValues) {
  String[] values = remoteCustomFieldValue.getValues();
  for (String value : values) {
    System.out.println("Value for CF with Id:" + remoteCustomFieldValue.getCustomfieldId() + " -" + value);
  }
}

这里,remoteCustomFieldValue.getValues()返回自定义字段值的字符串表示数组。

附件和 SOAP

在本例中,我们将看到如何通过 SOAP 向问题添加附件并浏览现有的附件。

准备工作

如同之前的示例,创建一个 JIRA SOAP 客户端。同时,确保在 JIRA 实例中启用了附件功能。

如何操作...

自 JIRA4 以来,附件是通过addBase64EncodedAttachmentsToIssue方法添加到问题中的,而 JIRA4 之前使用的是addAttachmentsToIssue方法。后者仍然可用,但已被弃用。此外,后者存在一个已知问题,在处理大型附件时会失败。

以下是使用addBase64EncodedAttachmentsToIssue方法向问题添加附件的步骤:

  1. 使用要上传的文件路径创建一个 File 对象。文件应通过有效的 URL 访问。

    File file = new File("var/tmp/file.txt");
    

    路径应该在上下文中是有效的。

  2. 将文件内容读取到字节数组中:

    // create FileInputStream object
    FileInputStream fin = new FileInputStream(file);
    
    /*
    * Create byte array large enough to hold the content of the file.
    * Use File.length to determine size of the file in bytes.
    */
    fileContent = new byte[(int) file.length()];
    
    /*
    * To read content of the file in byte array, use int read(byte[]
    * byteArray) method of java FileInputStream class.
    */
    fin.read(fileContent);
    fin.close();
    
  3. 使用BASE64Encoder从字节数组创建一个编码字符串。

    String base64encodedFileData = new BASE64Encoder().encode(fileContent);
    
  4. 对于所有需要上传的附件,执行步骤 1步骤 3,并创建一个包含所有编码数据的字符串数组。在我们的例子中,我们只有一个:

    String[] encodedData = new String[] { base64encodedFileData };
    
  5. addBase64EncodedAttachmentsToIssue方法中使用encodedData

    boolean attachmentAdded = jiraSoapService.addBase64EncodedAttachmentsToIssue(authToken, ISSUE_KEY, new String[] { "test.txt" }, encodedData);
    

    其中 ISSUE_KEY 是附件所在问题的关键字,字符串数组(第三个参数)包含将用于存储附件名称的字符串。

addAttachmentsToIssue方法也可以使用,类似于前面提到的方法,除了我们不对数据进行编码。与将文件作为编码数据的字符串数组发送不同,这个方法需要将文件作为字节数组的数组发送。以下是步骤:

  1. 像之前一样将文件读取到字节数组中:

    File file = new File(filePath);
    FileInputStream fin = new FileInputStream(file);
    fileContent = new byte[(int) file.length()];
    fin.read(fileContent);
    fin.close();
    
  2. 创建这些字节数组的数组,并将读取的文件添加到其中:

    byte[][] files = new byte[1][];
    files[0] = fileContent;
    
  3. 你可以读取任意数量的文件,每个文件将作为字节数组读取,并添加到文件数组中。

  4. 调用addAttachmentsToIssue方法:

    boolean attachmentAdded = jiraSoapService.addAttachmentsToIssue(authToken, ISSUE_KEY, new String[] {"test.txt" }, files);
    

浏览问题上的附件可以通过getAttachmentsFromIssue方法完成。它返回一个RemoteAttachment对象数组,从中可以提取附件的详细信息,如nameid等。然后,我们可以使用获取的信息构建附件的 URL。以下是步骤:

  1. 获取RemoteAttachment对象的数组:

    RemoteAttachment[] attachments = jiraSoapService.getAttachmentsFromIssue(authToken, ISSUE_KEY);
    

    这里,ISSUE_KEY是我们正在浏览的课题的关键字。

  2. 可以从RemoteAttachment对象中读取附件的信息。

    System.out.println("Attachment Name:" + remoteAttachment.getFilename() + ", Id:"+ remoteAttachment.getId());
    

    可以按照以下方式构建 JIRA 实例中附件的 URL:

    System.out.println("URL: "+ BASE_URL+ "/secure/attachment/" + remoteAttachment.getId() + "/"+ remoteAttachment.getFilename());
    

    这里,BASE_URL是 JIRA 的基础 URL,包括上下文路径。

通过 SOAP 进行工作日志和时间追踪

JIRA 中的时间追踪是一个很棒的功能,它允许用户追踪他们在特定问题上花费的时间。它让用户可以在花费时间时记录工作,JIRA 会追踪原定估计时间、实际花费时间和剩余时间。如果需要,用户还可以调整剩余的时间!

虽然 JIRA 提供了一个很好的用户界面让用户记录他们正在进行的工作,但有时,比如与第三方产品集成时,使用 SOAP 记录工作是必要的。在这个教程中,我们将展示如何使用 SOAP API 记录工作。

准备工作中…

如前面教程所示,创建一个 JIRA SOAP 客户端。同时,确保在 JIRA 实例中启用了时间追踪功能。

如何操作…

根据我们需要如何处理问题上的剩余估算,有不同的方法可供选择来记录工作。在所有情况下,我们都需要创建一个RemoteWorklog对象来保存我们记录的工作详情。以下是步骤:

  1. 创建一个包含所需详情的RemoteWorklog对象:

    RemoteWorklog worklog = new RemoteWorklog();
    worklog.setTimeSpent("1d 3h");
    worklog.setComment("Some comment!");
    worklog.setGroupLevel("jira-users");
    worklog.setStartDate(new GregorianCalendar(2011, Calendar.MAY, 10));
    

    注意,setStartDate()方法接收的是Calendar对象,而不是 Javadocs 中提到的Date对象。

  2. 使用适当的方法来添加之前的工作日志。例如,如果你想自动调整问题上的剩余估算,可以使用addWorklogAndAutoAdjustRemainingEstimate方法:

    RemoteWorklog work = jiraSoapService.addWorklogAndAutoAdjustRemainingEstimate(authToken, ISSUE_KEY, worklog);System.out.println("Added work:" + work.getId());
    
  3. 如果你想保留剩余估算,可以使用addWorklogAndRetainRemainingEstimate

    RemoteWorklog work = jiraSoapService.addWorklogAndRetainRemainingEstimate(authToken, ISSUE_KEY, worklog);
    
  4. 如果你想添加一个新的剩余估算,可以使用addWorklogWithNewRemainingEstimate

    RemoteWorklog work = jiraSoapService.addWorklogWithNewRemainingEstimate(authToken, ISSUE_KEY, worklog, "1d");
    
  5. 这将会把工作日志添加为1d 3h,并将剩余估算重置为1d(1 天),无论原来的估算是多少。

如何操作…

在第一步中,我们使用了设置方法来填充字段。正如你可能已经猜到的,最重要的字段是timeSpent,它指定了一个 JIRA 时长格式的时间段,表示在工作日志上花费的时间。在我们的例子中,我们使用了1d 3h,即表示 1 天 3 小时。

如同之前的代码一样,我们还可以为记录的工作指定startDategroupLevelroleId,这些用于限制记录工作可见性并添加评论。注意,不应在对象上设置 ID,因为当工作日志添加到问题上时,ID 将自动生成。此外,可见性只能为一个组或角色设置,而不能同时为两者设置。

还有更多…

更新和删除工作日志的操作方式相同,可以使用以下方法:

  • updateWorklogAndAutoAdjustRemainingEstimate

  • updateWorklogAndRetainRemainingEstimate

  • updateWorklogWithNewRemainingEstimate

  • deleteWorklogAndAutoAdjustRemainingEstimate

  • deleteWorklogAndRetainRemainingEstimate

  • updateWorklogWithNewRemainingEstimate

以下是更新和删除调用的示例:

jiraSoapService.updateWorklogWithNewRemainingEstimate(authToken, work, "1d");
jiraSoapService.deleteWorklogAndRetainRemainingEstimate(authToken, work.getId());

可以使用getWorklogs方法浏览问题上所有现有的工作日志,该方法返回一个RemoteWorklog对象的数组。

通过 SOAP 在问题上评论

在这个教程中,我们将看到如何管理问题上的评论。

准备工作

按照第一个教程中提到的方法,创建一个 JIRA SOAP 客户端。

如何操作…

使用 SOAP 在问题上添加评论的方法如下:

  1. 创建一个RemoteCommentobject并使用 setter 方法设置必要的字段。

    final RemoteComment comment = new RemoteComment();
    comment.setBody(COMMENT_BODY);
    //comment.setRoleLevel(ROLE_LEVEL); // Id of your project role
    comment.setGroupLevel(null); // Make it visible to all
    

    请注意,ID 不应在对象上设置,因为它将在评论创建时自动生成。同时,可见性只能针对组或角色设置,不能同时针对两者设置。

  2. 将评论添加到问题中:

    jiraSoapService.addComment(authToken, ISSUE_KEY, comment);
    

    可以通过getComments方法获取问题的评论,该方法返回一个RemoteComment对象的数组。

    RemoteComment[] comments = jiraSoapService.getComments(authToken, ISSUE_KEY);
    for (RemoteCommentremoteComment : comments) {
      System.out.println("Comment:" + remoteComment.getBody() + " written by " + remoteComment.getAuthor());
    }
    

评论可以使用editComment操作进行编辑,但我们应先通过使用hasPermissionToEditComment方法检查是否具有编辑权限,如下所示:

// Check permissions first
if (jiraSoapService.hasPermissionToEditComment(authToken, comment)) {
  comment.setBody(COMMENT_BODY + " Updated");
  comment.setGroupLevel("jira-users"); 
  jiraSoapService.editComment(authToken, comment);
}

删除评论尚未通过 SOAP 暴露!

通过 SOAP 进行用户和组管理

现在让我们看看如何使用 SOAP 进行用户和组管理。当需要从第三方应用程序管理用户和组时,这非常有用。

准备工作

创建一个如前所述的 SOAP 客户端。

如何操作...

创建组和用户非常简单。以下是客户端创建后的操作方式:

//Create group jtricks-test-group
RemoteGroup group = jiraSoapService.createGroup(authToken, "jtricks-test-group", null);
//Create user jtricks-test-user
RemoteUser user = jiraSoapService.createUser(authToken, "jtricks-test-user", "password", "Test User", "support@j-tricks.com");

这里,第一个代码片段创建了一个名为jtricks-test-group的组。第三个参数是一个RemoteUser,当组创建时,可以将其作为第一个用户添加到该组。如果组需要为空创建,则可以将其设置为 null。

第二个代码片段创建了一个用户,并提供了相关的详细信息,例如Name(名称)、Password(密码)、Full Name(全名)和Email(电子邮件)。

可以通过以下方式将用户添加到组中:

jiraSoapService.addUserToGroup(authToken, group, user);

这里,组和用户分别是RemoteGroupRemoteUser对象。

可以通过以下方式获取现有的用户或组:

RemoteUser user = jiraSoapService.getUser(authToken, "jtricks-test-user");
RemoteGroup group = jiraSoapService.getGroup(authToken, "jtricks-test-group");

可以从RemoteGroup对象中获取组中的用户,如下所示:

RemoteUser[] users = group.getUsers();
for (RemoteUser remoteUser : users) {
  System.out.println("Full Name:"+remoteUser.getFullname());
}

删除用户或组也很简单,如下所示:

//Delete User.
jiraSoapService.deleteUser(authToken, user1.getName());
//Delete Group. 
jiraSoapService.deleteGroup(authToken, group1.getName(), group.getName());

这里,swapGroup标识了要更改评论和工作日志可见性的组。

使用 SOAP 推进问题的工作流

这是每个人在将 JIRA 与第三方应用程序集成时都希望实现的目标。为了满足不同的使用场景,问题的状态需要进行更改,正确的做法是通过工作流推进问题。

推进操作将把问题移动到适当的状态,并触发相应的后置函数和事件。在本教程中,我们将学习如何执行相同的操作。

准备工作

如果尚未创建 SOAP 客户端,请照常创建一个。

如何操作...

JIRA 暴露了progressWorkflowAction方法,用于通过工作流推进问题。以下是操作步骤:

  1. 确定我们应该从当前状态执行的动作 ID。对于每个问题状态,都有一个与之相关的步骤,并且可以有零个或多个过渡到工作流中的其他步骤。

    动作 ID 可以从工作流屏幕中识别,在方括号内与过渡名称一起显示,如下图所示:

    如何操作...

    上一张截图显示了 JIRA 的默认工作流,解决 问题操作的 ID 是5,它是从打开状态发起的。请注意,如果不同状态中的操作没有共享相同的操作,那么相同的操作可能有不同的 ID。因此,在进行步骤 2之前,识别操作 ID 是很重要的。

    当你实现一个完整应用程序时,可能需要在客户端存储这些操作 ID,因为 JIRA 没有暴露基于当前状态检索操作 ID 的方法。

    ID 还可以通过查找 XML 中的action元素来获取:

    <action id="5" name="Resolve Issue" view="resolveissue">
    

    可以通过点击 XML 链接从管理 | 工作流屏幕将工作流导出为 XML。

  2. 确定在过渡过程中需要修改的字段,并为每个字段创建一个RemoteFieldValue对象。你只能修改那些在工作流过渡中可用的字段。

    在我们的示例中,我们使用 JIRA 默认工作流中的解决 问题操作,它与解决 屏幕相关联。我们在屏幕上有AssigneeResolution字段,因此我们可以为它们创建RemoteFieldValue对象,如下所示:

    RemoteFieldValue field1 = new RemoteFieldValue("resolution", new String[] { "3" });
    RemoteFieldValue field2 = new RemoteFieldValue("assignee", new String[] { "jobinkk" });
    

    RemoteFieldValue接受一个 ID 和一个字符串数组,表示我们需要设置的值。在我们的示例中,字段是单值字段,因此数组中只有一个元素。多维字段,如级联字段,应该使用我们在本章早些时候更新自定义字段时所看到的完全限定 ID。标准字段 ID 的完整列表可以在IssueFieldConstants类中找到。任何不在过渡屏幕中的字段都会被忽略。

  3. 使用这些属性执行progressWorkflowAction

    RemoteIssue updatedtissue = jiraSoapService.progressWorkflowAction(authToken, ISSUE_KEY, "5", new RemoteFieldValue[] { field1, field2 });
    

    在结束此配方之前,可以通过RemoteIssue对象使用getStatus方法找到问题的当前状态。

    System.out.println("Progressed "+updatedtissue.getKey()+ " to " + updatedtissue.getStatus() + " status!");
    

通过 SOAP 管理版本

我们已经看过如何将版本添加为问题的修复版本或受影响版本。但是,如何使用 SOAP 创建这些版本呢?在这个配方中,我们将学习如何在项目中创建版本并管理它们!

准备工作

和往常一样,创建 SOAP 客户端。

如何操作...

可以通过以下方式将新版本添加到项目中:

  1. 使用必要的详细信息创建一个RemoteVersion对象:

    RemoteVersion remoteVersion = new RemoteVersion();
    remoteVersion.setName("Test Release");
    remoteVersion.setReleaseDate(new GregorianCalendar(2011, Calendar.MAY, 10));
    remoteVersion.setSequence(5L);
    

    这里,sequence定义了版本在版本列表中出现的顺序。

  2. 使用addVersion方法创建版本:

    RemoteVersion createdVersion = jiraSoapService.addVersion(authToken, "TST", remoteVersion);
    System.out.println("Created version with id:"+createdVersion.getId());
    

    其中TST是新版本创建所在项目的键。

一旦版本被创建,你可以使用releaseVersion方法发布版本。它需要一个RemoteVersion作为输入,并且需要设置发布标志。

createdVersion.setReleased(true);
jiraSoapService.releaseVersion(authToken, "TST", createdVersion);

同样的方法可以用于unrelease一个版本。你需要做的就是将released标志设置为false

如果released标志被设置为true且版本已经发布,则会抛出错误。尝试unrelease尚未发布的版本时也是相同的情况。

归档版本的过程与发布版本类似。在这里,传递的是归档标志作为参数,而不是设置发布标志。同时,使用的是版本名称而不是RemoteVersion对象。

// Archives version with name "Test release" in project with key JRA
jiraSoapService.archiveVersion(authToken, "JRA", "Test Release", true);

可以使用getVersions方法检索项目中的所有版本,该方法返回一个RemoteVersion对象的数组。

RemoteVersion[] versions = jiraSoapService.getVersions(authToken, "JRA");

SOAP API 中的管理方法

在我们结束对 SOAP API 中各种有用方法的讨论之前,可以看看管理方法。在这个示例中,我们将集中讨论一些与创建项目和权限相关的方法。其余方法在我们理解了本示例中的方法后,也能轻松阅读。

准备工作

按照我们在前面示例中讨论的方式创建 SOAP 客户端。

如何操作...

我们可以看看创建权限方案的过程,使用它创建项目并将一些用户添加到项目角色中。项目创建过程中使用的其他方案,如通知方案和问题安全方案,通过 SOAP 不支持。

以下是我们旅程的步骤:

  1. 创建新的权限方案:

    RemotePermissionScheme permScheme = jiraSoapService.createPermissionScheme(authToken, "Test P Scheme", "Test P Description");
    

    在这里,我们使用createPermissionScheme方法,通过传递认证令牌、名称和描述来创建一个新的权限方案。请注意,我们也可以通过使用getPermissionSchemes方法获取现有的权限方案列表。

  2. 使用addPermissionTo方法向新创建的权限方案添加相关权限。如果我们正在创建新的权限方案,则此步骤是必要的:

    RemotePermissionScheme modifiedPermScheme = jiraSoapService.addPermissionTo(authToken, permScheme, adminPermission, user);
    

    在这里,adminPermission应该是一个RemotePermission对象,位于通过getAllPermissions方法检索到的RemotePermission对象列表中。例如,管理 项目权限可以通过以下方式获取:

    RemotePermission[] permissions = jiraSoapService.getAllPermissions(authToken);
    RemotePermission adminPermission = null;
    for (RemotePermission remotePermission : permissions) {
      if (remotePermission.getPermission().equals(23L)) {
        adminPermission = remotePermission;
        break;
      }
    }
    

    在这里,23L管理 项目权限的 ID。其他权限的 ID 可以在com.atlassian.jira.security.Permissions类中找到。

    addPermissionTo方法的最后一个参数是一个RemoteEntity,它可以是RemoteUserRemoteGroup对象。我们在前面的示例中已经看过如何通过 SOAP 访问用户和组。在我们的示例中,我们通过如下方式根据名称获取一个用户:

    RemoteUser user = jiraSoapService.getUser(authToken, "jobinkk");
    
  3. 使用createProject方法创建项目:

    RemoteProject project = jiraSoapService.createProject(authToken, "TEST", "Test Name", "Test Description", "http://www.j-tricks.com", "jobinkk", permScheme, null, null);
    

    以下是参数:

    token - 认证令牌

    key - 项目密钥

    name - 项目名称

    description - 项目描述

    url - 项目的 URL

    lead - 项目负责人

    permissionScheme - 项目的权限方案,类型为 RemotePermissionScheme

    notificationScheme - 项目的通知方案,类型为 RemoteScheme

    issueSecurityScheme - 项目的问题安全方案,类型为 RemoteScheme

    对于这些方案,我们使用新创建的权限方案,并将其他两个方案设置为 null。我们可以通过创建一个RemoteScheme对象并填入相关方案的正确 ID 来指定特定的通知方案或问题安全方案。

  4. 向新创建的项目添加一个演员:

    jiraSoapService.addActorsToProjectRole(authToken, new String[] { "jobinkk" }, adminRole, project,	"atlassian-user-role-actor");
    

    这里,addActorsToProjectRole方法接受一个演员数组(在此案例中仅为jobinkk),演员要加入的角色,已经创建的项目,以及演员的类型。

    可以通过如下所示的角色 ID 来获取项目角色:

    RemoteProjectRoleadminRole = jiraSoapService.getProjectRole(authToken, 10020L);
    

    演员类型可以是atlassian-user-role-actoratlassian-group-role-actor,具体取决于我们在数组中添加的演员是用户还是组。

我们现在应该已经创建了带有新权限方案的项目,并且我们已将成员添加到相关角色中。

工作原理...

一旦方法执行完成,我们可以看到如下创建的项目:

工作原理...

新的权限方案会按照下面的截图创建:

工作原理...

这里只添加了一个权限,即管理项目的权限。我们可以以类似的方式添加其他权限。

同样,项目成员会按照下面的截图添加:

工作原理...

如你所见,除了我们添加的演员,默认演员也将成为成员的一部分!

SOAP API 中还有很多其他有用的方法,可以在以下网址找到:docs.atlassian.com/software/jira/docs/api/rpc-jira-plugin/latest/com/atlassian/jira/rpc/soap/JiraSoapService.html

确保查看与你的 JIRA 版本对应的 Java 文档!

在 JIRA 中部署 SOAP 服务

到目前为止,我们已经看到了多种通过 SOAP 在 JIRA 中执行不同操作的方法。但如果是 SOAP 不支持的操作怎么办?那种会妨碍你将 JIRA 与第三方应用程序集成的小问题呢?这时,RPC 端点 插件 模块就派上用场了。

RPC 端点插件模块使我们能够在 JIRA 中部署新的 SOAP 和 XML-RPC 端点。新增的端点不会成为现有 WSDL 的一部分,而是会在一个新的 URL 上提供,因此,如果你想访问新方法和其他现有方法,你将需要访问这两个 Web 服务。

在这个教程中,我们将展示如何部署一个新的 SOAP 端点来执行一个新的操作。

准备工作

使用 Atlassian Plugin SDK 创建一个骨架插件。写这篇文章时,PRC 插件仍然是 v1,所以如果jira.atlassian.com/browse/JRA-22596尚未解决,请确保创建 v1 插件!

此外,确保在管理 | 常规配置下启用了接受 远程 API 调用选项。

如何做...

让我们创建一个 SOAP RPC 插件,暴露一个新方法getProjectCategories,该方法用于获取 JIRA 实例中的所有项目类别。这是一个简单的方法,但希望能帮助我们涵盖创建新的 SOAP RPC 端点的所有基本知识。

  1. pom.xml中添加 RPC 插件依赖,以便获取现有的 RPC 类。根据需要修改版本。

    <dependency>
      <groupId>atlassian-jira-rpc-plugin</groupId>
      <artifactId>atlassian-jira-rpc-plugin</artifactId>
      <version>3.13-1</version>
      <scope>provided</scope>
    </dependency>
    

    这个 JAR 是 JIRA 安装的一部分。所以,如果你的 Maven 构建在查找 JAR 时失败,只需导航到WEB-INF/lib文件夹,并将 JAR 安装到本地 Maven 库中,如下所示:

    mvn install:install-file -DgroupId=atlassian-jira-rpc-plugin -DartifactId=atlassian-jira-rpc-plugin -Dversion=3.13-1 -Dpackaging=jar -Dfile=atlassian-jira-rpc-plugin-3.13-1.jar
    
  2. atlassian-plugin.xml中声明新的 RPC 服务。

    <rpc-soap key="jtricks-soap-service" name="JTricks SOAP Service" class="com.jtricks.JTricksSoapServiceImpl">
      <description>JTricks SOAP service.</description>
      <service-path>jtricksservice</service-path>
      <published-interface>com.jtricks.JTricksSoapService</published-interface>
    </rpc-soap>
    

    在这里,SOAP RPC 插件模块有一个独特的key,并为你的 SOAP 模块声明了一个新的interface,以及相应的实现class。在这种情况下,我们有JTricksSoapServiceJTricksSoapServiceImpl。服务路径jtricksservice定义了在 URL 命名空间中服务将被发布的位置,并会出现在 WSDL 的 URL 中。

  3. 为这个新类创建一个Component Plugins模块,以避免客户端出现空指针异常:

    <component key="jtricks-soap-component" name="JTricks SOAP Component" class="com.jtricks.JTricksSoapServiceImpl">
      <interface>com.jtricks.JTricksSoapService</interface>
    </component>
    
  4. 如下所示,在接口中声明新方法:

    public interface JTricksSoapService {
      String login(String username, String password);   
      // Method to return Project Categories
      RemoteCategory[] getProjectCategories(String token) throws RemoteException;
    }
    

    如你所见,我们已经添加了一个名为getProjectCategories的方法,该方法返回一个RemoteCategory对象的数组。我们还添加了一个登录方法,以便通过访问新的 WSDL 进行测试。

  5. 创建RemoteCategory bean。确保新的 bean 扩展了AbstractNamedRemoteEntity类。该 bean 应包含所有必需的属性,并为其定义 getter 和 setter。AbstractNamedRemoteEntity类已经暴露了name字段,因此RemoteCategory也会拥有它。我们将添加一个新的字段description

    public class RemoteCategory extends AbstractNamedRemoteEntity {
      private String description;
      public RemoteCategory(GenericValue value) {
        super(value);
        this.description = value.getString("description");
      }
    
      public void setDescription(String description) {
        this.description = description;
      }
    
      public String getDescription() {
        return description;
      }
    }
    

    如你所见,构造函数接受一个GenericValue并从中设置描述。在超类AbstractNamedRemoteEntity中,name也以类似的方式设置。

  6. 在实现类中实现getProjectCategories方法:

    public RemoteCategory[] getProjectCategories(String token) throws RemoteException {
      validateToken(token);
    
      Collection<GenericValue> categories = projectManager.getProjectCategories();
      RemoteCategory[] remoteCategories = new RemoteCategory[categories.size()];
    
      int i = 0;
      for (GenericValue category : categories) {
        remoteCategories[i++] = new RemoteCategory(category);
      }
      return remoteCategories;
    }
    

    在这里,我们所做的只是获取项目类别的集合,并返回一个通过类别GenericValue对象初始化的RemoteCategory对象数组。请注意,从 JIRA 4.4 开始,getProjectCategories()方法已经弃用,建议使用返回ProjectCategory对象集合的getAllProjectCategories()方法,而非GenericValue

    如果你注意到的话,我们在返回类别之前首先验证了token。验证过程如下:

    private void validateToken(String token) {
      try {
        User user = tokenManager.retrieveUser(token);
      } catch (RemoteAuthenticationException e) {
        throw new RuntimeException("Error Authenticating!,"+e.toString());
      } catch (RemotePermissionException e) {
      throw new RuntimeException("User does not have permission for this operation,"+e.toString());
    }
    }
    

    我们通过令牌获取用户,如果令牌无效,则抛出相应的错误。可以在构造函数中注入ProjectManagerTokenManager类,如下所示:

    public JTricksSoapServiceImpl(ProjectManagerprojectManager, TokenManagertokenManager) {
      this.projectManager = projectManager;
      this.tokenManager = tokenManager;
    }
    

    请注意,从 JIRA 4.4 开始,应该使用retrieveUserNoPermissioncheck方法而非retrieveUser,因为某些 JIRA 实例可能希望允许匿名访问。各个方法将执行权限检查。

  7. 实现login方法以返回令牌。

    public String login(String username, String password) {
      try {
        return tokenManager.login(username, password);
      } catch (RemoteAuthenticationException e) {
        throw new RuntimeException("Error Authenticating!,"+e.toString());
      } catch (com.atlassian.jira.rpc.exception.RemoteException e) {
        throw new RuntimeException("Couldn't login,"+e.toString());
      }
    }
    

    它仅使用TokenManager返回由用户名和密码创建的token

    编译插件并进行部署。由于是 v1,请确保插件被放置到WEB-INF/lib文件夹中。

它是如何工作的...

一旦插件部署完成,新的 WSDL 应可以通过以下链接访问:{your_jira_url}/rpc/soap/jtricksservice?WSDL

工作原理...

如你所见,我们通过接口暴露的新方法现在可以在 WSDL 文件中看到,位于前面截图中圈出的地方。

在 JIRA 中部署 XML-RPC 服务

在前一个示例中,我们已经看到如何在 JIRA 中部署 SOAP 服务。在本示例中,我们将看到如何部署 XML-RPC 服务。

准备工作

使用 Atlassian 插件 SDK 创建一个骨架插件。在这里,我们再次开发的是 v1 插件。因此,确保atlassian-plugin.xml中没有Version 2属性。

是的,确保接受 远程 API 调用选项已开启,如前一个示例所示。

如何操作...

与 SOAP 插件一样,我们将暴露一个新方法getProjectCategories,该方法用于检索项目中的所有项目类别。以下是步骤:

  1. pom.xml中添加 RPC 插件依赖项,以便获取现有的 RPC 类。相应地更改版本:

    <dependency>
      <groupId>atlassian-jira-rpc-plugin</groupId>
      <artifactId>atlassian-jira-rpc-plugin</artifactId>
      <version>3.13-1</version>
      <scope>provided</scope>
    </dependency>
    
  2. atlassian-plugin.xml中声明新的 RPC 服务:

    <rpc-xmlrpc key="xmlrpc" name="JTricks XML-RPC Services" class="com.jtricks.XmlRpcServiceImpl">
      <description>The JTricks sample XML-RPC services.</description><service-path>jtricks</service-path>
    </rpc-xmlrpc>
    

    在这里,我们定义了一个XmlRpcServiceImpl类和一个service-path。该服务路径jtricks用于访问新的方法,而不是用于访问现有方法的默认jira1路径。

  3. 为该类创建一个接口,将XmlRpcServiceImpl命名为XmlRpcService并在其中定义新方法。

    public interface XmlRpcService {	
      String login(String username, String password) throws Exception;    
      Vector getprojectCategories(String token);
    }
    

    和之前一样,我们有一个login方法。如果你注意到,getprojectCategories方法的返回类型是Vector,而不是RemoteCategory对象的数组。

    RPC 接口中的所有方法,如果返回的是对象列表,应该返回Vector;如果返回的是单一对象(GenericValue),则应该返回HashTableVector将由一个或多个Hashtable组成,每个Hashtable代表列表中的一个GenericValue

  4. 定义RemoteCategory,如前一个示例中所定义的那样。我们将从RemoteCategory对象的数组创建一个Vector,用于返回项目类别的详细信息:

    public class RemoteCategory extends AbstractNamedRemoteEntity {
      private String description;
    
      public RemoteCategory(GenericValue value) {
        super(value);
        this.description = value.getString("description");
      }
    
      public void setDescription(String description) {
        this.description = description;
      }
    
      public String getDescription() {
        return description;
      }
    }
    
  5. 实现XmlRpcServiceImpl类。getprojectCategories方法的实现如下:

    public Vector getprojectCategories(String token) {
      validateToken(token);
    
    Collection<GenericValue> categories = projectManager.getProjectCategories();
      RemoteCategory[] remoteCategories = new RemoteCategory[categories.size()];
    
      int i = 0;
      for (GenericValue category : categories) {
        remoteCategories[i++] = new RemoteCategory(category);
      }
      return RpcUtils.makeVector(remoteCategories);
    }
    

    在这里,我们创建了一个RemoteCategory对象的数组,然后使用RpcUtils工具类从中创建一个Vector。该类在后台将RemoteCategory对象的数组转换为一个HashtableVector,每个Hashtable代表一个RemoteCategory

    如果我们希望返回单个RemoteCategory对象,而不是一个数组,我们应该将其作为一个Hashtable返回,构造方法如下:

    RpcUtils.makeStruct(remoteCategory);
    

    如前所述,使用 JIRA4.4 中的getAllProjectCategories方法。实现loginvalidateToken方法,如前一个示例所讨论的那样。

  6. 编译插件并部署它。由于是 v1 插件,确保插件被放置到WEB-INF/lib文件夹中。

工作原理...

插件部署后,可以使用新的服务路径jtricks.getprojectCategories访问新方法。关于如何访问 XML-RPC 方法的更多细节将在下一个示例中介绍。

另请参阅

  • 编写一个 Java XML-RPC 客户端

编写一个 Java XML-RPC 客户端

在前面的示例中,我们展示了如何创建一个 SOAP 客户端,并使用它从外部第三方应用程序连接到 JIRA。我们还展示了如何通过 SOAP 和 XML-RPC 接口在 JIRA 中暴露新方法。在本示例中,我们将展示如何从用 Java 编写的客户端应用程序中调用 XML-RPC 方法。

XML-RPC 客户端的 Javadocs 可在以下链接找到:docs.atlassian.com/software/jira/docs/api/rpc-jira-plugin/latest/com/atlassian/jira/rpc/xmlrpc/XmlRpcService.html

准备工作

确保在 JIRA 的管理|全局设置中,接受 远程 API 调用选项已设置为开启

如何实现...

让我们尝试使用 JIRA 中部署的 XML-RPC 服务来检索项目列表。以下是步骤:

  1. 创建一个 Maven2 项目并添加Apache2 xml-rpc库的依赖。

    <dependency>
      <groupId>xmlrpc</groupId>
      <artifactId>xmlrpc</artifactId>
      <version>1.1</version>
    </dependency>
    

    请注意,我们在本示例中使用的xml-rpc库版本是 1.1 版。

  2. 创建一个 Java 客户端。在这个示例中,我们将创建一个独立的 Java 类,并将所有库放在classpath中。

  3. 实例化XmlRpcClient对象:

    XmlRpcClientrpcClient = new XmlRpcClient(JIRA_URI + RPC_PATH);
    

    这里,JIRA_URI 是你的 JIRA 实例的 URI,例如 jira.atlassian.comRPC_PATH 将是/rpc/xmlrpc,即使是通过插件暴露的新方法,它也将保持不变。在这种情况下,完整路径将是:jira.atlassian.com/rpc/xmlrpc

    请注意,我们在这里使用的是 XML-RPC v2。请查看你所使用版本的语法!

  4. 通过调用如下面所示的login方法登录到 JIRA:

    // Login and retrieve logon token
    Vector loginParams = new Vector(2);
    loginParams.add(USER_NAME);
    loginParams.add(PASSWORD);
    String loginToken = (String) rpcClient.execute("jira1.login", loginParams);
    System.out.println("Logged in: " + loginToken);
    

    正如你从 Javadocs 中可以了解到的,方法需要一个用户名和密码,这些信息作为 Vector 对象传递给客户端的 execute 方法。第一个参数是方法名,前面带有暴露方法的命名空间。在这种情况下,它是jira1,相当于我们在前面示例中看到的服务路径。因此,完整的方法名将是jira1.login

    对于login方法,返回的对象是一个身份验证令牌,它是一个 String 对象。

  5. 使用getProjectsNoSchemes方法检索项目列表:

    // Retrieve projects
    Vector loginTokenVector = new Vector(1);
    loginTokenVector.add(loginToken);
    List projects = (List) rpcClient.execute("jira1.getProjectsNoSchemes",   loginTokenVector);
    

    这里,我们需要像上面一样,发送一个Vector作为输入,并且方法名也在其中。在这种情况下,Vector中将包含身份验证令牌。如果需要调用一个方法,该方法需要一个复杂的对象(例如创建问题的场景),我们应当创建一个HashTable,将输入参数作为键值对,并将其添加到Vector中。

    该返回类型在此情况下被强制转换为List。这将是一个List类型的 map 对象,每个 map 代表一个RemoteProject,其中包含项目的详细信息作为键/值对。例如,可以使用键 name 从 map 中访问项目名称,如下一步所示。

  6. 从列表中获取项目的详细信息。详细信息将是通过RemoteProject对象中的 getter/setter 方法发布的项目属性,如名称、负责人等。

    for (Iterator iterator = projects.iterator(); iterator.hasNext();) {
      Map project = (Map) iterator.next();
      System.out.println(project.get("name") + " with lead " + project.get("lead"));
    }
    

    如前所述,详细信息可以作为键/值对从表示项目的 Map 对象中检索。这个逻辑适用于所有 XML-RPC 方法,其中复杂对象作为包含键/值对的 Map 对象进行检索。

  7. 从 JIRA 注销:

    Boolean bool = (Boolean) rpcClient.execute("jira1.logout", loginTokenVector);
    

    这里的输出被转换为 Boolean 类型,因为方法返回一个 Boolean 值。

  8. 如果我们尝试使用前面示例中暴露的新方法来获取类别列表,代码将类似于以下内容:

    // Retrieve Categories
    Vector loginTokenVector = new Vector(1);
    loginTokenVector.add(loginToken);
    List categories = (List) rpcClient.execute("jtricks.getprojectCategories", loginTokenVector);
    for (Iterator iterator = categories.iterator(); iterator.hasNext();) {
      Map category = (Map) iterator.next();
      System.out.println(category.get("name"));
    }
    

    请注意,这里方法名称的前缀是 jtricks,因为这是在 RPC Endpoint 插件模块中使用的服务路径。其他一切工作方式相同。

将服务和数据实体暴露为 REST API

现在我们已经了解了如何通过 SOAP 和 XML-RPC 接口暴露 JIRA 功能,是时候转向 REST API 了。与 RPC Endpoint 插件模块类型类似,JIRA 也有一个 REST 插件模块类型,使用它可以将服务或数据暴露到外部世界。

在这个示例中,我们将展示如何通过 REST 接口暴露我们在前面的示例中使用过的getProjectCategories方法。

准备就绪

使用 Atlassian Plugin SDK 创建一个骨架插件。插件应该是 v2 版本才能正常工作。

如何操作...

以下是创建一个 REST 插件以暴露getProjectCategories方法的逐步过程。

  1. 将所需的 REST maven 依赖项添加到pom.xml文件中:

    <dependency>
      <groupId>javax.ws.rs</groupId>
      <artifactId>jsr311-api</artifactId>
      <version>1.0</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>javax.xml.bind</groupId>
      <artifactId>jaxb-api</artifactId>
      <version>2.1</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.atlassian.plugins.rest</groupId>
      <artifactId>atlassian-rest-common</artifactId>
      <version>1.0.2</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
      <version>2.3</version>
      <scope>provided</scope>
    </dependency>
    

    请注意,所有的依赖项都是提供作用域的,因为它们已经在 JIRA 运行时中可用。

  2. 将 REST 插件模块添加到atlassian-plugin.xml中。

    <rest key="rest-service-resources" path="/jtricks" version="1.0">
      <description>Provides the REST resource for the tutorial plugin.</description>
    </rest>
    

    这里,路径和版本定义了资源仅在插件部署后才能访问的完整路径。在本例中,完整路径将变为BASE_URL/rest/jtricks/1.0/,其中BASE_URL是 JIRA 的基本 URL。

    定义将返回给客户端的数据。JAXB 注解用于将这些对象映射到 XML 和 JSON 格式。

    在我们的示例中,getCategories方法应返回一个 Category 对象的列表,因此我们需要定义一个 Categories 对象和一个 Category 对象,前者包含后者的列表。对于这两个对象,我们应使用注解。

  3. 按如下方式定义Category对象:

    @XmlRootElement
    public static class Category{
      @XmlElement
      private String id;
    
      @XmlElement
      private String name;
      public Category(){
      }
    
      public Category(String id, String name) {
        this.id = id;
        this.name = name;
      }
    }
    

    确保正确使用注解。@XmlRootElement注解将一个类或枚举类型映射为 XML 元素,这里用于categories@XmlElement将一个属性或字段映射为 XML 元素。其他可用的注解包括@XmlAccessorType@XmlAttribute,分别用于控制字段或属性是否默认序列化以及将属性或字段映射为 XML 属性。

    注意

    详细信息请参阅:jaxb.java.net/nonav/jaxb20-pfd/api/javax/xml/bind/annotation/package-summary.html

    确保提供一个公共的无参构造函数,以便在通过直接 URL 访问时正确渲染输出。另外,请注意,只有被注解的元素会通过 REST API 暴露。

  4. 定义Categories对象:

    @XmlRootElement
    public class Categories{
      @XmlElement
      private List<Category> categories;
    
      public Categories(){	
      }
    
      public Categories(List<Category> categories) {
        this.categories = categories;
      }
    }
    

    同样的规则也适用于这里。

  5. 创建Resource类。在包级别、类级别或方法级别,我们可以使用@Path注解来定义资源可用的路径。如果它在所有级别上都可用,最终的路径将是累积输出。

    这意味着如果你在包级别有@Path("/X"),在类级别有@Path("/Y"),在方法级别有@Path("/Z"),那么资源将通过以下路径访问:

    BASE_URL/rest/jtricks/1.0/X/Y/Z
    

    不同的方法可以具有不同的路径来区分它们。在我们的示例中,我们在类级别定义路径/categories

    package com.jtricks;
    ................
    
    @Path("/category")
    public class CategoryResource {
      ..................
    }
    
  6. 编写方法以返回Categories资源:

    @GET
    @AnonymousAllowed
    @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
    public Response getCategories() throws SearchException {  Collection<GenericValue> categories = this.projectManager.getProjectCategories();
      List<Category> categoryList =  new ArrayList<Category>();		for (GenericValue category : categories) {
        categoryList.add(new Category(category.getString("id"), category.getString("name")));
      }
      Response.ResponseBuilder responseBuilder = Response.ok(new Categories(categoryList));
      return responseBuilder.build();
    }
    

    如你所见,该方法没有@Path注解,因此将会在BASE_URL/rest/jtricks/1.0/category的 URL 上被调用。在这里,我们通常使用一个简单的 Bean 类构建Categories对象,然后使用ResponseBuilder来创建响应。

    前面提到的@GET注解表示类方法将处理 GET HTTP 请求。

    注意

    其他有效的注解包括 POST、PUT、DELETE 等,可以在以下网址查看详细信息:

    jsr311.java.net/nonav/javadoc/javax/ws/rs/package-summary.html

    @AnonymousAllowed表示该方法可以在不提供用户凭据的情况下调用。@Produces指定方法可能返回的内容类型。如果没有这个注解,方法可以返回任何类型。在我们的案例中,方法必须返回 XML 或 JSON 对象。

    另外两个有用的注解是:@PathParam@QueryParam@PathParam将方法变量映射到@Path中的一个元素,而@QueryParam将方法变量映射到查询参数。

    以下是我们如何使用它们的示例:

    @QueryParam
    
    

    以下是@QueryParam使用示例:

    @GET
    @AnonymousAllowed
    @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
    public Response getCategories(@QueryParam("dummyParam") String dummyParam) throws SearchException {
      System.out.println("This is just a dummyParam to show how parameters can be passed to REST methods:"+dummyParam);  ................
                       return responseBuilder.build();
    }
    

    在这里,我们获取名为dummyParam的查询参数,该参数可以在方法中使用。然后,资源将通过以下方式访问:BASE_URL/rest/jtricks/1.0/category?dummyParam=xyz

    在这种情况下,你将看到值 xyz 打印到控制台中。

    @PathParam
    
    @GET
    @AnonymousAllowed
    @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
    @Path("/{id}")
    public Response getCategoryFromId(@PathParam("id") String id) throws SearchException {
      GenericValue category = this.projectManager.getProjectCategory(new Long(id));  Response.ResponseBuilderresponseBuilder = Response.ok(new Category(category.getString("id"), category.getString("name")));
      return responseBuilder.build();
    }
    

    假设我们想在路径中传递一个类别的 ID,并仅获取该Category的详细信息;我们可以像之前所示那样使用PathParam。在这种情况下,指向该方法的 URL 将如下所示:

    BASE_URL/rest/jtricks/1.0/category/10010
    

    在这里,10010 是之前描述的方法中传递的类别 ID。

    当使用查询参数时,资源将不会被代理或浏览器缓存。所以,如果你传递一个 ID 来查找某种实体的信息,那么应该使用路径参数。这样该信息就会被缓存。

  7. 打包插件并部署它。

它是如何工作的...

如果你已经部署了同时包含getCategories()方法和之前看到的getCategoryFromId()方法的插件,那么可以通过以下 URL 检索类别列表:BASE_URL/rest/jtricks/1.0/category,如下面的截图所示:

它是如何工作的...

可以通过路径中的 ID 检索特定类别的详细信息,例如BASE_URL/rest/jtricks/1.0/category/10001,如下图所示:

它是如何工作的...

Atlassian 已发布了一些指南,网址为:confluence.atlassian.com/display/REST/Atlassian+REST+API+Design+Guidelines+version+1,这是在开发生产版本的 REST 服务插件之前非常有用的阅读材料。请查看:confluence.atlassian.com/display/REST/REST+API+Developer+Documentation获取更多详细信息。

为 REST API 编写 Java 客户端

在本教程中,我们将快速演示如何使用 REST API 创建一个 Java 客户端与 JIRA 进行通信。

准备就绪

确保在 JIRA 的管理 | 全局设置中启用了接受远程 API 调用选项。

如何做...

为了使用 REST API 连接到 JIRA,Atlassian 开发了一个名为 JRJC 的 JIRA REST Java 客户端库。它在 REST API 及相关的 HTTP(S)通信之上提供了一个薄层抽象,并提供了一个域对象模型来表示 JIRA 实体,例如问题、优先级、解决方案、状态、用户等等。REST API 和 JRJC 库目前处于 Alpha 阶段,并在快速发展!可以在以下链接查看该库的状态:studio.atlassian.com/wiki/display/JRJC/Home

我们将使用 JRJC 通过独立的 Java 程序连接到我们的 JIRA 实例。以下是步骤:

  1. 创建一个 Maven 项目,并将 JRJC 依赖项添加到pom.xml文件中。

    <dependency>
      <groupId>com.atlassian.jira</groupId>
      <artifactId>jira-rest-java-client</artifactId>
      <version>0.2.1</version>
    </dependency>
    

    确保使用适当版本的 JRJC。所有版本可以在 Maven 仓库中找到,网址为maven.atlassian.com/public/com/atlassian/jira/jira-rest-java-client/。如果您没有使用 Maven,完整的依赖项可以在 Atlassian 文档中找到,地址为studio.atlassian.com/wiki/display/JRJC/Project+Dependencies

  2. 如果您使用 Maven,可以通过运行maven eclipse:eclipse创建一个 Java 项目,或者使用您喜欢的 IDE 创建项目并将前面列出的所有依赖项添加到类路径中。完成后,创建一个独立的 Java 类。

  3. 创建与 JIRA 服务器的连接

    JerseyJiraRestClientFactory factory = new JerseyJiraRestClientFactory();
    URI uri = new URI("http://localhost:8080/jira");
    JiraRestClient jiraRestClient = factory.createWithBasicHttpAuthentication(uri, "username", "password");
    

    在这里,我们实例化了JerseyJiraRestClientFactory并使用createWithBasicHttpAuthentication方法,通过传入用户名和密码来实例化 REST 客户端。

    RESTful 架构提倡无状态连接,因此没有用户会话的概念。这意味着凭证将以明文形式在每个请求中来回传输,只是经过Base64编码,因此在防火墙外或公司网络之外使用时并不安全。

  4. 启动ProgressMonitor。所有的 REST 远程调用都将其作为参数。根据 Atlassian 文档,首先,它作为远程调用的明确标记,其次,未来他们计划使该接口能够报告进度,并取消(在可能的情况下)耗时过长的远程请求。

    目前,我们可以按如下方式启动它:

    NullProgressMonitor nullProgressMonitor = new NullProgressMonitor();
    

    获取执行操作所需的适当客户端。jiraRestClient暴露了一组客户端,如IssueRestClientProjectRestClientSearchClient等,每个客户端暴露了一组相关的操作。在这个例子中,我们将尝试检索一个问题,因此会使用IssueRestClient

    IssueRestClient issueRestClient = jiraRestClient.getIssueClient();
    
  5. 检索问题详情并打印。或者,根据需要执行相应的操作:

    Issue issue = issueRestClient.getIssue("TST-10", nullProgressMonitor);
    System.out.println(issue);
    

    这里,问题是com.atlassian.jira.rest.client.domain.Issue!

    可以在问题上执行各种其他操作,相关详情可以在 Javadocs 中找到,网址为docs.atlassian.com/jira-rest-java-client/0.2.1/apidocs/com/atlassian/jira/rest/client/IssueRestClient.html

    例如,我们可以按如下方式对问题进行投票:

    issueRestClient.vote(issue.getVotesUri(), nullProgressMonitor);
    

    该 API 可以通过docs.atlassian.com/jira-rest-java-client/0.2.1/apidocs/com/atlassian/jira/rest/client/IssueRestClient.html获取。

有些操作稍微复杂一些。例如,为了将问题推进到工作流中,您需要适当的转换 ID、转换过程中所需的字段以及可选的评论。我们可以按如下方式操作:

  1. 从问题中获取可用的转换。

    Iterable<Transition> transitions = issueRestClient.getTransitions(issue.getTransitionsUri(), nullProgressMonitor);
    
  2. 按照以下方式通过名称或 ID 找到相关的转换:

    private static Transition getTransitionByName(Iterable<Transition> transitions, String transitionName) {
      for (Transition transition : transitions) {
        if (transition.getName().equals(transitionName)) {
          return transition;
        }
      }
      return null;
    }
    
  3. 创建一个在转换过程中需要的字段列表。如果字段不是必需的,这个列表可以为空:

    Collection<FieldInput>fieldInputs = Arrays.asList(new FieldInput("resolution", "Done"));
    

    如有需要,创建一个Comment对象:

    Comment.valueOf("New comment");
    
  4. 按照以下方式转换问题:

    issueRestClient.transition(issue.getTransitionsUri(), new TransitionInput(startProgressTransition.getId(), fieldInputs, Comment.valueOf("New comment")),nullProgressMonitor);
    
  5. 你将看到通过执行我们选择的转换,问题在工作流中得到了推进。

类似地,可以使用相应的客户端执行各种方法。考虑到 JIRA REST API 发展得如此迅速,JRJC 具有很大的潜力,值得投入时间。

第十章:处理数据库

在本章中,我们将涵盖:

  • 使用自定义模式扩展 JIRA 数据库

  • 从插件访问数据库实体

  • 在 JIRA 数据库中持久化插件信息

  • 使用活动对象存储数据

  • 访问 JIRA 配置属性

  • 获取数据库连接以进行 JDBC 调用

  • 将自定义字段从一种类型迁移到另一种类型

  • 从数据库中检索问题信息

  • 从数据库中检索自定义字段详细信息

  • 从数据库中检索问题权限

  • 从数据库中检索工作流详细信息

  • 在数据库中更新问题状态

  • 从数据库中检索用户和组信息

  • 在数据库中处理变更历史

介绍

我们已经在第二章,理解 插件 框架中看到,JIRA 使用 Ofbiz 套件的 Entity Engine 模块来处理数据库操作。

OfBiz 代表“Open For Business”,OfBiz Entity Engine 是一套用于建模和管理特定实体数据的工具和模式。

根据关系数据库管理系统(RDBMS)标准实体关系建模概念的定义,实体是由一组字段和与其他实体的关系所定义的数据单元。

在 JIRA 中,这些实体定义在两个文件中,entitygroup.xmlentitymodel.xml,它们都位于WEB-INF/classes/entitydefs文件夹中。entitygroup.xml存储先前定义的组的实体名称。如果查看该文件,您将看到 JIRA 中的默认组名为default;您将在稍后看到的实体配置文件中找到相同的定义。entitymodel.xml包含实际的实体定义,相关细节将在后续的配方中进行说明。

实体配置定义在entityengine.xml文件中,该文件位于WEB-INF/classes文件夹中。正是在此文件中定义了datasource、事务工厂等。该文件的内容根据我们使用的数据库和应用服务器的不同而有所变化。例如,当数据库是 MySQL 且应用服务器是tomcat时,datasource的定义如下:

<datasource add-missing-on-start="true" check-fk-indices-on-start="false" check-fks-on-start="false" check-indices-on-start="true" check-on-start="true" field-type-name="mysql" helper-class="org.ofbiz.core.entity.GenericHelperDAO" name="defaultDS" use-foreign-key-indices="false" use-foreign-keys="false">
        <jndi-jdbc jndi-name="java:comp/env/jdbc/JiraDS" jndi-server-name="default"/>
</datasource>

有关连接到其他各种数据库的更多信息,请参阅confluence.atlassian.com/display/JIRA/Connecting+JIRA+to+a+Database

对于其他应用服务器,jndi-server属性在jndi-jdbc元素中有所不同,如下所示:

  • Orion 格式:

  • JBoss 格式:

  • Weblogic 格式:

transaction-factory标签的定义如下:

<transaction-factory class="org.ofbiz.core.entity.transaction.JNDIFactory">
      <user-transaction-jndi jndi-name="java:comp/env/UserTransaction" jndi-server-name="default"/>
      <transaction-manager-jndi jndi-name="java:comp/env/UserTransaction" jndi-server-name="default"/>
 </transaction-factory>

实体定义的 XML 文件在文件中通过entity-group-readerentity-model-reader属性进行引用,分别指向entitygroup.xmlentitymodel.xml

<entity-model-reader name="main">
<resource loader="maincp" location="entitydefs/entitymodel.xml"/>
</entity-model-reader>
<entity-group-reader name="main" loader="maincp" location="entitydefs/entitygroup.xml"/>

委托器元素也在此文件中定义,如下所示:

<delegator entity-group-reader="main" entity-model-reader="main" name="default">
        <group-map datasource-name="defaultDS" group-name="default"/>
</delegator>

不同数据库的字段类型映射 XML 也定义在此文件中。一个例子如下:

<field-type loader="maincp" location="entitydefs/fieldtype-mysql.xml" name="mysql"/>

阅读更多关于配置entityengine.xml的信息,见www.atlassian.com/software/jira/docs/latest/entityengine.html,以及关于实体建模概念的信息,见ofbiz.apache.org/docs/entity.html

在本节扩展 JIRA 数据库 自定义 架构中,我们还将看到 JIRA 数据库架构的简要介绍,详细内容可以在confluence.atlassian.com/display/JIRADEV/Database+Schema中查看。

扩展 JIRA 数据库的自定义架构

现在我们知道 JIRA 的方案定义保存在WEB-INF/classes/entitydefs/entitygroup.xmlentitymodel.xml中,让我们来看看如何扩展现有的方案定义。如果你想将一两个自定义表格添加到 JIRA 中,如何扩展 JIRA 方案?仅仅是在数据库中创建新表格就可以了吗?我们将在本节中了解这一点。

如何操作...

JIRA 使用在WEB-INF/classes/entitydefs/entitygroup.xmlentitymodel.xml文件中输入的方案定义。它不仅在验证和创建架构时使用这些文件,还在 JIRA 数据备份的导入和导出过程中使用这些文件。JIRA 还使用这些实体定义通过 OfBizDelegator(docs.atlassian.com/jira/latest/com/atlassian/jira/ofbiz/OfBizDelegator.html)读写数据库,相关细节我们将在接下来的配方中看到。

以下是将新表添加到 JIRA 架构的快速步骤。假设我们正在添加一个表格来保存员工的详细信息。

  1. 为表格确定一个实体名称。这可以与表格名称相同,也可以不同。这个名称将用于 XML 备份,并且也会被 OfBizDelegator 用于读取或写入数据。

    在我们的示例中,我们选择Employee作为实体名称。

  2. 修改WEB-INF/classes/entitydefs/entitygroup.xml文件以包含新的实体组定义:

    <entity-group group="default" entity="Employee"/>
    

    这里,group属性指的是委托人所关联的组名。你可以在WEB-INF/classes/entityengine.xml中找到它,如下所示:

    <delegator name="default" entity-model-reader="main" entity-group-reader="main"><group-map group-name="default" datasource-name="defaultDS"/></delegator>
    

    entity属性保存了实体的名称。

  3. 修改WEB-INF/classes/entitydefs/entitymodel.xml文件以包含新的实体定义:

    <entity entity-name="Employee" table-name="employee" package-name="">
      <field name="id" type="numeric"/>
      <field name="name" type="long-varchar"/>
      <field name="address" col-name="empaddress" type="long-varchar"/>
      <field name="company" type="long-varchar"/>
    
      <prim-key field="id"/>
      <index name="emp_entity_name">
        <index-field name="name"/>
      </index>
    </entity>
    

    在这里,entity-name属性保存了我们在步骤 2中使用的实体名称。table-name保存了表格名称;它是可选的,如果没有提供,将从entity-name中派生。package-name可用于在你希望将实体定义组织到不同的包中时使用。

    entity 元素包含每个需要创建的表的列的一个 field 元素。field 元素有一个 name 属性,保存字段的名称。如果字段的列名不同,可以使用 col-name 属性,例如员工地址的情况。如果缺少 col-name,则使用字段的名称。下一个重要的属性是 type。在我们的示例中,idnumeric 类型,而 nameaddresslong-varchar 类型。

    字段的这些类型定义映射到每种数据库类型的适当列类型。field-type 映射存储在 WEB-INF/classes/entitydefs/ 下,并在 entityengine.xml 中声明,如下所示:

    <field-type name="oracle10g" loader="maincp" location="entitydefs/fieldtype-oracle10g.xml"/>
    

    如果你查看 fieldtype-oracle10g.xml 文件,你会注意到 numeric 被映射到 NUMBER(18,0),而 long-varchar 被映射到 VARCHAR2(255)。你可以从同一个文件中找到各种映射,甚至是相关的 Java 数据类型。

    prim-key 元素用于定义表的主键约束,如前所示。在我们的示例中,id 是主键。对于我们创建的所有新表,主键必须命名为 id

    index 元素为该表指定的字段创建数据库索引。我们可以指定索引名称和需要索引的字段组。

    你还可以使用 relation 元素来定义实体之间的关系,如下所示:

    <relation type="one" title="Parent" rel-entity-name="Company">
      <key-map field-name="company" rel-field-name="id"/>
    </relation>
    

    在这里,我们通过声明一个员工只能有一个公司,添加了 Employee 实体和 Company 实体之间的关系。在上面的示例中,Employee 应该有一个指向公司记录 id 字段的 company 字段。换句话说,员工记录中的 company 字段将是公司记录的外键。

    关于实体定义的更多详细信息可以在 ofbiz.apache.org/docs/entity.html#Entity_Modeling 找到。

  4. 在进行更改后,重新启动 JIRA。

How it works...

当 JIRA 在应用了之前的更改后重新启动时,你会注意到启动时日志中出现一个警告信息,如下所示:

How it works...

一旦 JIRA 识别到数据库中没有与新实体名称 employee 对应的表,它将创建一个,如下所示:

How it works...

即使是索引信息也被存储,如下所示:

How it works...

如果你想向现有表中添加新列,可以像之前看到的那样添加字段定义,并且在重新启动 JIRA 时,表将更新以包含该列。

如果数据库中有表或表中的列没有有效的实体或字段定义在 entitymodel.xml 中,你将在 JIRA 日志中看到错误信息。

在 JIRA 升级时,必须小心更新 entitygroup.xmlentitymodel.xml 文件,否则更改将丢失。

从插件访问数据库实体

我们已经看到了 JIRA 数据库中各种实体是如何定义的,以及如何引入新实体。在本示例中,我们将看到如何使用这些实体定义从数据库中读取和写入数据。

如何实现...

JIRA 公开了OfBizDelegator (docs.atlassian.com/jira/latest/com/atlassian/jira/ofbiz/OfBizDelegator.html) 组件,它是org.ofbiz.core.entity.DelegatorInterface的封装,用于通过 Ofbiz 层与数据库进行通信。

你可以通过在构造函数中注入或通过ComponentManager来获取OfBizDelegator实例,如下所示:

OfBizDelegator delegator = ComponentManager.getInstance().getComponentInstanceOfType(OfBizDelegator.class);

从数据库读取

我们可以通过上述委托类公开的各种方法从数据库中读取。例如,之前示例中定义的员工表的所有记录可以这样读取:

List<GenericValue> employees = delegator.findAll("Employee");

这里,findAll方法接受实体名称(不是表名称),并返回一个GenericValue对象列表,每个对象代表表中的一行。可以通过字段名(而不是col-name)来读取个别字段,如下所示:

Long id = employees.get(0).getLong("id");
String name = employees.get(0).getString("name");

字段应转换为的数据类型可以从我们在上一个示例中看到的field-type映射 XML 中找到。

我们可以在满足特定条件时,使用findByAnd方法从数据库中读取数据:

List<GenericValue> employees = delegator.findByAnd("Employee", EasyMap.build("company","J-Tricks"));

这将返回所有公司名称为J-Tricks的记录。你可以使用findByCondition方法强制执行更复杂的条件,并且只选择感兴趣的字段,如下所示:

List<GenericValue> employees = this.delegator.findByCondition("Employee", new EntityExpr("id",EntityOperator.GREATER_THAN,"15000"), EasyList.build("id","name"));

这里,我们查找所有 ID 大于15000的员工记录,并且只检索员工的 ID 和姓名。

findListIteratorByCondition (docs.atlassian.com/jira/latest/com/atlassian/jira/ofbiz/OfBizDelegator.html#findListIteratorByCondition%28java.lang.String,%20org.ofbiz.core.entity.EntityCondition,%20org.ofbiz.core.entity.EntityCondition,%20java.util.Collection,%20java.util.List,%20org.ofbiz.core.entity.EntityFindOptions%29)

这个方法可以用来添加更多选项,如orderBy子句、EntityFindOptionswhere条件、having条件等,如下所示:

OfBizListIterator iterator = this.delegator.findListIteratorByCondition("Employee", new EntityExpr("id",EntityOperator.GREATER_THAN,"15000"), null, UtilMisc.toList("name"), UtilMisc.toList("name"), new EntityFindOptions(true, EntityFindOptions.TYPE_SCROLL_INSENSITIVE, EntityFindOptions.CONCUR_READ_ONLY, true));
List<GenericValue> employees = iterator.getCompleteList();
iterator.close();

在这里,我们搜索所有 ID 大于15000的记录。此情况下没有having条件,因此我们将其留为空值。接下来的两个参数指定仅需要选择name字段,并且记录应该按name字段排序。最后一个参数指定了EntityFindOptions。在这里,我们定义了包含四个参数的 EntityFindOptions,其中包括TYPE_SCROLL_INSENSTITVECONCUR_READ_ONLY。第一个 true 用于specifyTypeAndConcur,最后一个 true 用于 distinct 选择。

如果specifyTypeAndConcur为 true,接下来的两个参数将用于指定resultSetTyperesultSetConcurrency。如果为 false,则使用 JDBC 驱动程序的默认值。在上面的例子中,specifyTypeAndConcur为 true,因此resultSetType被设置为TYPE_SCROLL_INSENSITIVE,而resultSetConcurrency被设置为CONCUR_READ_ONLY。关于此以及可能的值,可以参考download.oracle.com/javase/tutorial/jdbc/basics/retrieving.html

如前所述,EntityFindOptions构造函数中的最后一个 true 用于选择不同的值。显然,这是使用 Entity Engine 进行 distinct 选择的唯一方法。你可以在实体引擎的食谱中找到更多信息,网址为www.opensourcestrategies.com/ofbiz/ofbiz_entity_cookbook.txt

不要忘记关闭迭代器,正如前面的代码片段所示。

写入新记录

使用 OfBizDelegator 在表中创建新记录非常简单,如下所示:

GenericValue newEmployee = this.delegator.createValue("Employee",EasyMap.build("name","Some Guy", "address","Some Address", "company","J-Tricks"));

确保不要提供 ID,因为它是自动生成的。此外,映射中缺失的字段将被设置为null。必须提供所有必填字段的数据,以避免错误。

更新记录

写入记录是通过检索记录、修改值并使用store()方法来完成的。例如,我们可以检索 ID 为12000的记录并修改它,如下所示:

GenericValue employee = delegator.findByAnd("Employee",   EasyMap.build("id","12000")).get(0);
employee.setString("name","New Name");
employee.store();

更多有用的方法可以在 Java 文档中找到,网址为docs.atlassian.com/jira/latest/com/atlassian/jira/ofbiz/OfBizDelegator.html

在 JIRA 数据库中持久化插件信息

在开发插件时,我们会遇到许多场景,需要存储有关插件的特定信息,无论是配置细节还是实体的元数据。我们如何在不创建自定义模式且不需要编辑实体定义的情况下做到这一点?在这个例子中,我们将展示如何利用 JIRA 现有的框架存储我们开发的插件的特定信息。

JIRA 使用 Open Symphony 的PropertySet框架将属性存储到数据库中。这些属性是一组键/值对,存储在用户想要的任何实体中。属性的键始终是一个字符串值;值可以是:String、Long、Date、Boolean 或 Double。我们已经在第二章,理解 插件 框架中看到 JIRA 是如何使用它的。在本食谱中,我们将看到如何使用PropertySet存储我们的自定义数据。

如何做到...

假设我们需要将一个布尔值存储到数据库中,作为插件配置的一部分,并稍后读取;以下是执行此操作的步骤:

  1. 获取PropertySet的实例,使用PropertiesManager

    PropertySet propertySet = PropertiesManager.getInstance().getPropertySet();
    

    从 JIRA 4.3 开始,PropertiesManager.getInstance()方法已被弃用。相反,您可以通过依赖注入将PropertiesManager注入构造函数,或者从ComponentManager中检索,如下所示:

    PropetySet propertySet =  ComponentManager.getComponent(PropertiesManager.class).getPropertySet();
    
  2. 使用setBoolean方法持久化布尔属性:

    propertySet.setBoolean("mt.custom.key1", new Boolean(true));
    

    同样,String、Long、Double 和 Date 类型的值可以通过相应的方法进行存储。

  3. 存储的属性可以在任何时刻检索,如下所示:

    Boolean key = propertySet.getBoolean("mt.custom.key1");
    

然而,如何将一个更复杂的结构(如属性)存储到现有实体中呢?假设我们想要存储一个用户的地址。JIRA 将用户信息存储到OSUser实体中,如下所示:

  1. 检索我们将存储地址的用户实体的 ID。例如,如果有一个用户jobinkk,我们可以从OSUser实体中找到该用户的 ID,它对应于 JIRA 中的userbase表。假设 ID 是10032

  2. 获取PropertySet的实例,使用PropertySetManager,传递我们获得的实体的详细信息:

    HashMap entityDetails = new HashMap();
    entityDetails.put("delegator.name", "default");
    entityDetails.put("entityName", "OSUser");
    entityDetails.put("entityId", 10032L);
    PropertySet userProperties = PropertySetManager.getInstance("ofbiz", entityDetails);
    

    在这里,我们创建一个包含实体名称(即OSUser)和用户 ID(即10032)的映射。我们还传递了在entityengine.xml中定义的委托名称,该文件位于WEB-INF/classes文件夹下,默认情况下即为此。然后我们从PropertySetManager中检索PropertySet实例,使用ofbiz作为键。

  3. 这些值可以像之前一样设置,具体取决于字段的类型。在这种情况下,我们将有多个键用于州、省,国家等:

    userProperties.setString("state", "Kerala");
    userProperties.setString("country", "India");
    

    这将被存储在相应的表中。

  4. 我们可以通过以类似的方式创建PropertySet并使用 getter 方法,稍后检索这些值:

    System.out.println("Address:" + userProperties.getString("state")+", "+userProperties.getString("country"));
    

它是如何工作的...

当使用从PropertiesManager实例化的PropertySet设置属性时,正如我们在布尔值的案例中所做的那样,它会存储在propertyentry表中,ENTITY_NAMEjira.propertiesENTITY_ID1。它还会有一个唯一的 ID,之后将用来将值存储到propertynumberpropertystringpropertytextpropertydate表中,具体取决于我们使用的数据类型。

在我们的案例中,propertyentry表已经填充了值,如下所示:

工作原理...

第一个是我们添加的 Boolean 属性,而第二个和第三个是用户属性。

布尔值会作为数字(01)存储,因此,propertyentry 表将 propertytype 存储为 1,表示数字值。对于布尔属性,propertynumber 表中有一个对应的条目,ID 为 11303,如下所示:

工作原理...

在我们的示例中,Boolean 被设置为 true,因此,propertynumber 存储值 1。如果设置为 false,则会存储 0

在地址的情况下,实体是 OSUser,它的 entityId10032。我们看到了两行,ID 分别为 1130411305,每行的 propertytype5,表示字符串值。由于它们是字符串值,所以被存储在 propertystring 表中,如下图所示:

工作原理...

希望这能帮助你大致了解我们如何在现有实体记录中存储属性。

使用 propertySet 的好处是我们不需要创建额外的模式或实体定义,并且这些属性在 JIRA 数据导出时会被导出到备份 XML 中。因此,以这种方式存储的所有配置,在数据重新导入到另一个 JIRA 实例时会被保留。

使用 Active Objects 存储数据

Active Objects 是 JIRA 最近使用的一项技术,用于允许每个插件存储数据。这为插件开发人员提供了一个真正受保护的数据库,他们可以将属于其插件的数据存储在其中,并且其他插件无法访问。在这个教程中,我们将看到如何使用 Active Objects 将地址实体存储到数据库中。

你可以在 java.net/projects/activeobjects/pages/Home 阅读更多关于 Active Objects 的信息。

准备就绪

使用 Atlassian Plugin SDK 创建一个骨架插件。

如何操作...

为了更好地理解,让我们看一下我们在之前的教程中使用的简单 "地址实体" 示例。这也有助于与 PropertySet 进行简便的比较,如果需要的话。按照以下步骤在插件中使用 Active Objects:

  1. pom.xml 中包含 Active Objects 依赖项。添加合适的 ao 版本:

    <dependency>
      <groupId>com.atlassian.activeobjects</groupId>
      <artifactId>activeobjects-plugin</artifactId>
      <version>${ao.version}</version>
      <scope>provided</scope>
    </dependency>
    
  2. pom.xml 文件中,按照如下方式将 Active Objects 插件工件包含在 maven-jira-plugin 配置下:

    <plugin>
     <groupId>com.atlassian.maven.plugins</groupId>
     <artifactId>maven-jira-plugin</artifactId>
     <version>3.0.6</version>
     <extensions>true</extensions>
     <configuration>
      <pluginArtifacts>
       <pluginArtifact>
        <groupId>com.atlassian.activeobjects</groupId>
        <artifactId>activeobjects-plugin</artifactId>
        <version>${ao.version}</version>
       </pluginArtifact>
       <pluginArtifact>
        <groupId>com.atlassian.activeobjects</groupId>
        <artifactId>activeobjects-jira-spi</artifactId>
        <version>${ao.version}</version>
       </pluginArtifact>
      </pluginArtifacts>
      <productVersion>${jira.version}</productVersion>
      <productDataVersion>${jira.data.version}</productDataVersion>
     </configuration>
    </plugin>
    
  3. 将 Active Objects 插件模块添加到 Atlassian 插件描述符中:

    <ao key="ao-module">
      <description>The configuration of the Active Objects service</description>
      <entity>com.jtricks.entity.AddressEntity</entity>
    </ao>
    

    如你所见,模块有一个唯一的键,并指向我们稍后将定义的实体,这里是 AddressEntity

  4. 包含一个 component-import 插件,将 ActiveObjects 注册为 atlassian-plugin.xml 文件中的一个组件,前面已经提到的模块:

    <component-import key="ao" name="Active Objects components" interface="com.atlassian.activeobjects.external.ActiveObjects">
      <description>Access to the Active Objects service</description>
    </component-import>
    
  5. 定义用于数据存储的实体。实体应该是一个接口,并且应继承 net.java.ao.Entity 接口。我们需要在这个实体接口中做的所有工作是为我们需要存储的数据定义 getter 和 setter 方法。

    例如,我们需要将名称、城市和国家作为地址实体的一部分进行存储。在这种情况下,AddressEntity 接口将如下所示:

    public interface AddressEntity extends Entity{
      public String getName();
      public void setName(String name);
    
      public String getState();   
      public void setState(String state);
    
      public String getCountry();
      public void setCountry(String country);
    }
    

通过这样做,我们已经设置了实体,以便存储所有三个属性。我们现在可以使用 ActiveObjects 组件创建、修改或删除数据。该组件可以通过注入到构造函数中进行实例化。

private ActiveObjects ao;

public ManageProperties(ActiveObjects ao) {
  this.ao = ao;
}

可以使用以下代码片段向数据库添加新行:

AddressEntity addressEntity =  ao.create(AddressEntity.class);
addressEntity.setName(name);
addressEntity.setState(state);
addressEntity.setCountry(country);
addressEntity.save();

可以通过使用 id(主键)或通过 net.java.ao.Query 对象查询数据来读取详细信息。使用 ID 就像下面这样简单:

AddressEntity addressEntity = ao.get(AddressEntity.class, id);

Query 对象可以按如下方式使用:

AddressEntity[] addressEntities = ao.find(AddressEntity.class, Query.select().where("name = ?", name));
for (AddressEntity addressEntity : addressEntities) {
  System.out.println("Name:"+addressEntity.getName()+", State:"+addressEntity.getState()+", Country:"+addressEntity.getCountry());
}

这里,我们正在查询所有具有给定名称的记录。

一旦通过任何方式获取到实体,我们可以通过使用 setter 方法简单地编辑内容:

addressEntity.setState(newState);
addressEntity.save();

删除甚至更简单!

ao.delete(addressEntity);

希望这能为活跃对象提供一个公平的介绍。

它是如何工作的...

在后台,每添加一个实体,JIRA 数据库中就会为其创建一个单独的表。活跃对象服务与这些表进行交互以完成工作。

如果你查看数据库,将会看到每个名为 MyObject 的实体(属于插件密钥为 com.example.ao.myplugin 的插件)会创建一个名为 AO_{SOME_HEX}_MY_OBJECT 的表,其中:

  • AO 是一个常见的前缀

  • SOME_HEX 是插件密钥 com.example.ao.myplugin 的哈希值的前六个十六进制字符的集合

  • MY_OBJECT 是实体类名 MyObject 的大写翻译

对于每个在实体接口中定义的具有 getter 方法的属性 getSomeAttribute,都会在表中创建一列,列名为 SOME_ATTRIBUTE,遵循 Java Beans 命名约定——通过下划线分隔两个单词,并将它们都保持大写。

在我们的 AddressEntity 示例中,已创建以下表:ao_d6b86e_address_entity

它是如何工作的...

对于我们的示例,数据存储方式如下所示:

它是如何工作的...

访问 JIRA 配置属性

我们已经在之前的配方中看到如何使用 PropertySet 存储插件的详细信息。在这个配方中,我们将看到如何使用 PropertySet 访问 JIRA 配置属性。

如何操作...

JIRA 中有很多全局配置设置,这些设置通过管理菜单进行配置。更多关于各种选项的信息可以在 confluence.atlassian.com/display/JIRA/Configuring+Global+Settings 阅读。JIRA 将这些信息存储在哪里,如何访问?

所有这些配置属性,例如 General ConfigurationBase URLAttachments pathlicense info 等,都存储在我们之前看到的 propertyset 表中。它们与虚拟实体 jira.properties 一起存储。这与使用 PropertiesManager 检索 PropertySet 时使用的虚拟实体相同,正如我们在持久化插件信息时所看到的那样。

在这里,所有属性键条目都存储在 propertyentry 表中,jira.properties 是实体名称,entityid1。每个属性的 propertytype 不同,取决于存储的内容。例如,jira.option.allowattachments 是一个标志,因此存储在 propertynumber 表中,其值为 01。在这种情况下,propertytype1,表示数字 value。另一方面,jira.path.index 存储一个字符串,表示索引路径,其 propertytype5,值存储在 propertystring 表中。

所有属性可以通过以下 SQL 命令访问:

select * from propertyentry where ENTITY_NAME='jira.properties';

如果您只想查看字符串类型的属性及其值,可以使用以下命令:

select PROPERTY_KEY, propertyvalue from propertyentry pe, propertystring ps where pe.id=ps.id and pe.ENTITY_NAME='jira.properties' and propertytype='5';

如果您想查找特定的属性,可以使用以下命令:

select PROPERTY_KEY, propertyvalue from propertyentry pe, propertynumber pn where pe.id=pn.id and pe.ENTITY_NAME='jira.properties' and pe.PROPERTY_KEY='jira.option.allowattachments';

注意,应使用适当的属性表,propertynumber 在此情况下!

在插件中也可以实现相同的功能,如下所示:

  1. 检索 PropertySet 对象:

    PropertySet propertySet = PropertiesManager.getInstance().getPropertySet();
    

    如前所述,从 JIRA 4.3 开始,PropertiesManager.getInstance() 方法已被弃用。相反,您可以通过依赖注入将 PropertiesManager 注入构造函数,或从 ComponentManager 中检索,示例如下:

    PropetySet propertySet =  ComponentManager.getComponent(PropertiesManager.class).getPropertySet();
    
  2. 所有属性键可以通过以下方式检索:

    Collection<String> keys = propertySet.getKeys();
    
  3. 同样,特定类型的所有属性可以通过以下方式访问:

    Collection<String> stringKeys = propertySet.getKeys(5);
    
  4. 可以按如下方式访问特定键的值:

    String attachmentHome = propertySet.getString("jira.path.attachments");
    boolean attachmentsAllowed = propertySet.getBoolean("jira.option.allowattachments");
    

获取 JDBC 调用的数据库连接

使用 OfBizDelegator 获取我们所需的所有详细信息并不总是可行。如果我们需要通过 JDBC 执行复杂的查询怎么办?在这个示例中,我们将看到如何获取在 entityengine.xml 中定义的数据库连接。

如何做到这一点...

如果您熟悉 JDBC,数据库连接查找非常简单。按照以下步骤快速获取连接:

  1. 创建 javax.naming.InitialContext 对象:

    InitialContext cxt = new InitialContext();
    
  2. 使用 EntityConfigUtil 从实体配置中检索数据库信息:

    DatasourceInfo datasourceInfo = EntityConfigUtil.getDatasourceInfo ("defaultDS");
    

    在这里,defaultDSentityengine.xml 中定义的数据源的名称。

  3. DataSourceInfo 对象中检索 jndi-name 字符串:

    String jndiName = datasourceInfo.jndiJdbcElement.getAttribute ( "jndi-name" );
    
  4. 使用 jndi-name 查找 javax.sql.DataSource 对象:

    DataSource ds = ( DataSource ) cxt.lookup ( jndiName );
    
  5. 从 DataSource 创建 java.sql.Connection 对象:

    Connection conn = ds.getConnection();
    
  6. 一旦建立连接,它与其他任何 JDBC 调用相似。创建语句或准备语句并执行它们。

在我写这篇文章时,JIRA 4.3 正在发布,并且连接将变得更加简单。只需执行以下操作:

Connection conn = new DefaultOfBizConnectionFactory().getConnection();

简单吧?

DataSourceInfo 可以通过以下方式访问:

DatasourceInfo datasourceInfo = new DefaultOfBizConnectionFactory().getDatasourceInfo();

轮到你了,明智地编写 JDBC 调用吧!

将自定义字段从一种类型迁移到另一种类型

JIRA 中的自定义字段有多种类型——文本字段、选择列表、数字字段等等。我们可能会遇到需要更改字段类型的场景,但又不想丢失已经输入的数据!这可能吗?在一定程度上是可能的。在这个教程中,我们将看到如何做到这一点。

字段的类型只能通过数据库更改,因为 UI 不支持此操作。但是,并不是所有字段类型都能支持转换。例如,无法将文本字段转换为数字字段,因为该字段已有的所有值可能不是数字字段。然而,反过来是可以的,因为所有数字值都可以当作文本值来处理。类似地,你可以将选择字段转换为文本字段,但不能将多选字段转换为文本字段,因为多选字段有多个值,每个值在 customfieldvalue 表中都有单独的行。

所以,第一步是通过查看源类型和目标类型,确定转换是否可行。如果可行,我们就可以按照这个教程的描述继续修改类型。

如何操作...

以下步骤概述了如何修改自定义字段的类型,前提是源类型和目标类型满足我们之前讨论的条件:

  1. 停止 JIRA 实例。

  2. 以 JIRA 用户身份连接到 JIRA 数据库。

  3. 通过执行如下 SQL 脚本,修改 customfield 表中的自定义字段键:

    update customfield set customfieldtypekey = 'com.atlassian.jira.plugin.system.customfieldtypes:textfield' where cfname = 'Old Number Value';
    

    在这里,将名为 'Old Number Value' 的自定义字段类型更改为文本字段。确保自定义字段的名称唯一;如果不是,请在 where 条件中使用自定义字段 ID。

  4. 类似地,修改搜索器键,并使用适当的搜索器。在前面的案例中,我们需要将搜索器值修改为文本搜索器,如下所示:

    update customfield set customfieldsearcherkey = 'com.atlassian.jira.plugin.system.customfieldtypes:textsearcher' where cfname = 'Old Number Value';
    
  5. 提交更改并断开连接。

  6. 启动 JIRA。

  7. 通过进入 管理 | 系统 | 索引,对 JIRA 实例进行完全重新索引。

自定义字段现在应从旧的数字字段修改为文本字段。添加或更新值并进行搜索以验证更改。

从数据库中获取问题信息

关于一个问题的信息分散在 JIRA 数据库的多个表中。然而,一个好的起点是 jiraissue 表,这里存储着问题记录。它有外键引用其他表,同时,问题 ID 也会在其他几个表中被引用。

以下图表展示了 jiraissue 表与其他表之间的父子关系:

从数据库中获取问题信息

如你所见,关于问题的关键信息,如项目、问题类型、状态、优先级、解决方案、安全级别、工作流等,都存储在各自的表中,但都通过jiraissue表引用,使用外键。外键指向其他表的 ID,但这些表上没有强制执行外键约束。

类似地,以下图表显示了jiraissue表与哪些表有子关系:

从数据库中检索问题信息

这里,customfieldvaluechangegroupjiraactionlabelworklogfileattachmentissuelinktrackback_ping等表,都有一个名为issueidissue(或 source 或 destination)的外键,指向相关问题的 ID。

在本食谱中,我们将学习如何借助前面的图表访问一些问题的信息。

如何操作...

当表之间存在父子关系时,我们可以进行联接操作,以获取大部分所需的信息。例如,可以通过以下查询获取所有问题及其项目名称:

select ji.id, ji.pkey, pr.pname from jiraissue ji inner join  project pr on ji.project = pr.id;

在这里,我们通过条件进行内部连接,即项目的 ID 与jiraissue表中的项目列值相同。

类似地,所有关于一个问题的评论可以通过以下查询获取:

select ji.pkey, ja.actionbody, ja.created, ja.author from jiraissue ji left join jiraaction ja on ji.id = ja.issueid;

在此示例中,我们检索问题的评论及其作者和创建日期。相同的方法可以应用于前面图表中的所有表。

还有更多...

访问问题的版本和组件信息稍有不同。尽管你会在jiraissue表中看到fixforcomponent列,它们已经不再使用!

每个问题可以有多个版本或组件,因此在jiraissueversion/component表之间存在一个join表,称为nodeassociationsource_node_entity将是ISSUEsource_node_id表示问题 ID。sink_node_entity将在此情况下为ComponentVersion,而sink_node_id将存储相应组件或版本的 ID。

还有第三列association_type,它的值将是IssueFixVersionIssueVersionIssueComponent,分别用于版本修复、受影响版本或组件。

我们可以通过以下方式访问问题的组件:

select ji.pkey, comp.cname from nodeassociation na, component comp, jiraissue ji where comp.id = na.sink_node_id and ji.id = na.source_node_id and na.association_type = "IssueComponent" and ji.pkey = 'DEMO-123';

这里,DEMO-123是问题。我们还可以以类似的方式检索受影响的版本和修复版本。

从数据库中检索自定义字段详细信息

在之前的食谱中,我们已经看过如何从数据库中检索问题的标准字段。在本食谱中,我们将学习如何检索问题的自定义字段详细信息。

JIRA 中的所有自定义字段都存储在customfield表中,正如我们在修改自定义字段类型时所见。某些自定义字段,例如选择字段、多选字段等,可以配置不同的选项,并且这些选项可以在customfieldoption表中找到。

对于每个自定义字段,都可以配置一组上下文。这些上下文指定了与字段相关联的项目或问题类型列表。对于每个这样的上下文,fieldconfigscheme中会有一条带有唯一 ID 的记录。对于每个fieldconfigscheme,在configurationcontextfieldconfigschemeissuetype表中都会有记录,configurationcontext存储字段在相关上下文中与哪些项目相关联,fieldconfigschemeissuetype则存储字段与哪些问题类型相关联!对于像选择字段(Select)和多选字段(Multi Select)这样的字段,可以为不同的上下文配置不同的选项,这些选项可以在customfieldoption表中找到,通过customfildconfig列,它指向fieldconfigscheme表中的相应行。

每个配置方案必须在configurationcontextfieldconfigschemeissuetype中有记录。如果该方案没有限制到任何项目或问题类型,则相关表中的projectissuetype列应该为NULL

对于单个问题,自定义字段的值存储在customfieldvalue表中,并引用了jiraissuecustomfield表。对于多值字段,例如多选框、多个选择项等,customfieldvalue表中会有多条记录。

我们通过一个简单的图表来捕捉这些信息,如下所示:

从数据库中检索自定义字段详情

如何操作...

一旦添加了自定义字段,可以通过以下简单的查询从customfield表中检索该字段的详情:

select * from customfield where cfname = 'CF Name';

如果是具有多个选项的字段,例如选择字段,可以使用简单的连接查询检索选项,如下所示:

select cf.id, cf.cfname, cfo.customvalue from customfield cf inner join customfieldoption cfo on cf.id = cfo.customfield where cfname = 'CF Name';

可以通过以下方式从fieldconfigscheme表中检索各种字段配置:

select * from fieldconfigscheme where fieldid = 'customfield_12345';

这里,12345是自定义字段的唯一 ID。

与自定义字段相关联的项目可以通过以下方式检索:

select project.pname from configurationcontext inner join project on configurationcontext.project = project.id where fieldconfigscheme in  (select id from fieldconfigscheme where fieldid = 'customfield_12345');

当项目为NULL时,字段是全局性的,因此对所有项目都可用!

类似地,可以通过以下方式检索与字段相关联的问题类型:

select issuetype.pname from fieldconfigschemeissuetype inner join issuetype on fieldconfigschemeissuetype.issuetype = issuetype.id where fieldconfigscheme in (select id from fieldconfigscheme where fieldid = 'customfield_12345');

从数据库中检索问题的权限

JIRA 在对问题执行权限控制方面非常强大。它提供了许多配置选项来控制谁可以做什么。所有这些配置选项都围绕着 JIRA 中的两种不同方案展开,权限 方案问题 安全 方案

权限方案强制实施项目级安全性,而问题安全方案则强制实施问题级安全性。你可以授予查看项目中问题的访问权限,同时隐藏部分问题不让用户看到。然而,反过来就不可行,即,如果用户原本没有权限查看项目中的问题,则无法授予对某些特定问题的访问权限。

存储 JIRA 数据库中权限信息的各种表格,以及它们之间的关系,可以如下所示:

从数据库中检索问题权限

如你所见,权限方案和问题安全方案都通过 nodeassociation 表与项目相关联。在这里,SOURCE_NODE_ENTITY项目,相应的 SOURCE_NODE_ID 存储项目的 ID。SINK_NODE_ENTITYPermissionSchemeIssueSecurityScheme,取决于方案类型。SINK_NODE_ID 将指向适当的方案。ASSOCIATION_TYPE 在两种情况下都是 ProjectSheme

对于每个权限方案,都有多个预定义的权限,例如,管理员 项目浏览 项目创建 问题,等等。对于每个这些权限,perm_typeperm_parameter 存储具有相关权限的实体类型及其值。例如,perm_type 可以是组、用户、项目角色等,而 perm_parameter 将分别是组名、用户名或项目角色。多个权限类型可以被授予单一权限。

同样,问题安全方案保存了多个安全级别,这些安全级别存储在 schemeissuesecuritylevels 表中。每个安全级别可以包含不同的实体,它们也使用 typeparameter 值定义;在这种情况下,列名分别是 sec_typesec_parameter

权限方案根据问题所在的项目强制实施,而安全方案则通过查看问题所分配的安全级别来强制实施。jiraissue 表中的安全列保存了此信息。

让我们看看如何根据前面的示意图从问题中检索一些信息。

如何操作...

通过 nodeassociation 表,找到与项目关联的权限方案非常容易,如下所示:

select pr.pname, ps.name from nodeassociation na, project pr, permissionscheme ps where pr.id = na.source_node_id and ps.id = na.sink_node_id and na.association_type = 'ProjectScheme' and na.source_node_entity = 'Project' and na.sink_node_entity = 'PermissionScheme';

同样,问题安全方案可以按如下方式检索:

select pr.pname, iss.name from nodeassociation na, project pr, issuesecurityscheme iss  where pr.id = na.source_node_id and iss.id = na.sink_node_id and na.association_type = 'ProjectScheme' and na.source_node_entity = 'Project' and na.sink_node_entity = 'IssueSecurityScheme';

与权限方案中的特定权限相关的权限参数,id 值为 9,可以轻松地按如下方式检索:

select sp.perm_type, sp.perm_parameter from schemepermissions sp inner join permissionscheme ps on sp.scheme = ps.id where ps.id = 9 and sp.permission = 23

在这里,sp.permission = 23 表示 PROJECT_ADMIN 权限。不同的权限类型可以在 com.atlassian.jira.security.Permissions 类中找到。这里,perm_type 表示权限是否授予给组、用户或角色;perm_parameter 存储相应的组名、用户名或角色名。

同样,可以编写查询来检索问题安全方案的信息。例如,可以按如下方式检索问题安全方案中每个级别的安全级别、安全类型和参数:

select iss.name, sisl.name, sis.sec_type, sis.sec_parameter from issuesecurityscheme iss , schemeissuesecurities sis, schemeissuesecuritylevels sisl where sis.scheme = iss.id and sisl.scheme =iss.id;

编写更复杂的查询超出了本书的范围,但希望前面的模式图和示例 SQL 图表提供了足够的信息来入门!

从数据库中检索工作流详情

其他人通常在数据库中查找的主要信息是关于工作流的。一个问题的当前状态是什么?如何查找一个问题所关联的工作流?工作流 XML 存储在哪里?在本节中,我们将快速浏览与工作流相关的表。

正如我们在前面的章节中看到的,JIRA 工作流包含状态、步骤和转换。状态和步骤之间总是存在一对一的映射,并且它们始终保持同步。然后,存在将问题从一个步骤移动到另一个步骤的转换,从而从一个状态转换到另一个状态。

工作流本身以 XML 文件的形式存储在 jiraworkflows 表中。JIRA 使用 OSWorkflow API 处理这些 XML 文件,以检索每个转换、步骤等所需的信息。任何草稿工作流都会存储在 jiradraftworkflows 表中。

jiraissue 表保存其当前状态的 ID,状态的详细信息存储在 issuestatus 表中。我们可以使用 jiraissue 表中的状态 ID 从 issuestatus 表中检索对应的详细信息。

jiraissue 还有另一个列,workflow_id,它指向与该问题关联的工作流以及该问题所在的工作流的当前步骤。第一个信息,即问题所关联的工作流,存储在 os_wfentry 表中。在这里,workflow_id 将指向 os_wfentry 表的 ID 列。第二个信息,即与问题关联的当前步骤,存储在 os_currentstep 表中。在这里,workflow_id 指向 os_currentstep 表中的 entry_id 列。

所以,每个问题在 os_wfentryos_currentstep 表中都有一个条目。它们之间的关系是:jiraissue.WORKFLOW_ID == OS_WFENTRY.IDjiraissue.WORKFLOW_ID == OS_CURRENTSTEP.ENTRY_ID

还有另一个表格 os_history 步骤,它保存了一个问题经过的所有步骤的历史信息。在这里,workflow_id 再次指向 os_historystep 表中的 entry_id 列。通过这个表,我们可以检索到一个问题在特定步骤或状态下停留的时间。

以下的模式图展示了重要的关系:

从数据库中检索工作流详情

如何做...

一个问题的状态,DEMO-123,可以通过一个简单的查询来获取,如下所示:

select istat.pname from issuestatus istat, jiraissue ji where istat.id=ji.issuestatus and ji.pkey='DEMO-123';

关联到问题的工作流的详细信息可以按如下方式检索:

select * from os_wfentry where id=(select workflow_id from jiraissue where pkey='DEMO-123');

您可以使用以下查询检索问题的工作流 XML:

select ji.pkey, wf.descriptor from jiraissue ji, jiraworkflows wf, os_wfentry osw where ji.workflow_id = osw.id and osw.name = wf.workflowname and ji.pkey='DEMO-123';

当前与问题关联的步骤可以通过以下方式检索:

select * from os_currentstep where entry_id = (select workflow_id from jiraissue where pkey = 'DEMO-123');

工作流状态(步骤)变化的历史记录可以从os_historystep中检索,如下所示:

select * from os_historystep where entry_id = (select workflow_id from jiraissue where pkey = 'DEMO-123');

在数据库中更新问题状态

在本配方中,我们将简要了解如何更新 JIRA 数据库中问题的状态。

准备就绪

请参考前面的配方,了解 JIRA 中与工作流相关的表。

如何操作...

请参阅以下步骤以更新 JIRA 中问题的状态:

  1. 停止 JIRA 服务器。

  2. 连接到 JIRA 数据库。

  3. jiraissue表中更新issuestatus字段,设置所需的状态:

    UPDATE jiraissue SET issuestatus = (select id from issuestatus where pname = 'Closed') where pkey = 'DEMO-123';
    
  4. os_currentstep表中修改step_id,使其与前一步骤中使用的状态相关联的步骤 ID 相匹配。step_id可以在工作流 XML 中找到,步骤名称位于括号内,如下所示截图所示:如何操作...

    如您所见,JIRA 默认工作流中的Closed状态与id值为 6 的Closed步骤相关联。现在,step_id可以按以下方式更新:

    UPDATE os_currentstep SET step_id = 6 where entry_id = (select workflow_id from jiraissue where pkey = 'DEMO-123');
    

    这里,我们修改os_currentstep中的step_id,其中entry_idjiraissue表中的workflow_id相同。

    这非常重要,因为步骤和状态应该始终保持同步。单独更新状态会改变问题的状态,但会阻止后续的工作流操作。

  5. 如果您希望跟踪状态变化,请在os_historystep字段中添加条目。这是完全可选的。忽略它不会导致任何问题,只是后期无法报告这些记录。

  6. 相应地更新os_currentstep_prevos_historystep_prev表。这些表保存前一条记录的 ID。这个步骤是可选的。

  7. 提交更改并启动 JIRA。

  8. 通过进入管理 | 系统 | 索引,执行完整的重新索引。

从数据库中检索用户和组

当外部用户管理未开启,我们可以通过运行一些简单的 SQL 查询,从数据库中找到所有 JIRA 用户及其组的信息。在本配方中,我们将查看涉及的各种表。

在 JIRA 4.3 版本之前,用户信息存储在userbase表中,组信息存储在groupbase表中,关于哪些用户属于哪些组的详细信息存储在membershipbase表中。

在那些版本中,用户属性是使用PropertySet存储的,正如我们之前在某个配方中看到的(我们为用户添加了一个地址)。propertyentry表中会有一个用户条目,entity_nameOSUserentity_iduserbase表中用户的 ID。存储的属性示例包括全名和电子邮件地址,并且它们作为字符串值存储在propertystring表中。

还有一个表,userassociation,存储关于关注问题和投票问题的信息。在此表中,source_name列保存唯一的用户名,sink_node_id保存问题的 ID。sink_node_entity的值为Issueassociation_type的值为WatchIssueVoteIssue,具体取决于操作。

从 4.3 版本开始,JIRA 使用嵌入式 Crowd作为其用户管理框架。在这里,用户存储在cwd_user表中,组存储在cwd_group表中,成员关系存储在cwd_membership表中。在 4.3+版本中,可能会有组-用户关系或组-组关系,这些信息也存储在cwd_membership表中。此外,与之前的版本不同,存储属性的表是分开的——cwd_user_attributes用于存储用户属性,cwd_group_attributes用于存储组属性。

JIRA 4.3+版本还引入了用户目录的概念。一个 JIRA 实例可以拥有多个目录,并且不同目录中可以有相同的名称。目录的详细信息存储在cwd_directory表中,其属性存储在cwd_directory_attribute表中。cwd_user表和cwd_group表中都存在名为directory_id的引用字段,指向相应的目录 ID。cwd_directory_operation表存储与目录相关的操作,基于用户权限。

当不同目录中有多个用户具有相同名称时,JIRA 只会识别优先级最高目录中的用户。优先级存储在directory_position列中。

在 4.3 之前,表关系过于简单,无法绘制 ER 图,因此我们将为 4.3+版本绘制一张:

从数据库中检索用户和组

在 JIRA 4.3+版本中,关注和投票的工作方式与之前的版本相同。

如何操作...

由于表结构简单,通过直接访问数据库列出用户、组或它们的关系非常容易。例如,在 4.3 之前,我们只需运行以下命令,就能找出一个组中的所有用户:

select user_name from membershipbase where group_name = 'jira-administrators';

在 4.3+版本中,我们可以执行相同的操作,如下所示:

select child_name from cwd_membership where parent_name='jira-administrators' and membership_type = 'GROUP_USER' and directory_id = 1;

这里,我们还考虑目录的情况,因为我们可以在不同的目录中拥有相同的用户和组。

在 4.3 之前,诸如全名和电子邮件等属性存储在并从propertystring表中访问,如下所示:

select pe.property_key, ps.propertyvalue from propertystring ps inner join propertyentry pe on ps.id = pe.id  where pe.entity_name = 'OSUser' and pe.entity_id = (select id from userbase where userbase.username = 'username'); 

在 4.3+版本中,这些属性是cwd_user表的一部分,但也可能有其他属性存储在cwd_user_attributes表中,例如最后登录时间、无效密码尝试次数等,可以通过如下命令访问:

select attribute_name, attribute_value from cwd_user_attributes where user_id = (select id from cwd_user where user_name = 'someguy' and directory_id =1);

在所有版本中,关注问题的用户可以通过以下方式检索:

select source_name from userassociation where association_type = 'WatchIssue' and sink_node_entity = 'Issue' and sink_node_id = (select id from jiraissue where pkey='DEMO-123');

类似地,用户关注的所有问题可以通过以下方式检索:

select ji.pkey from jiraissue ji inner join userassociation ua on ua.sink_node_id =  ji.id where ua.association_type = 'WatchIssue' and ua.sink_node_entity = 'Issue' and ua.source_name = 'someuser';

对于投票也是一样,唯一不同的是关联类型为VoteIssue

在数据库中处理变更历史

在本章结束之前,我们还需要简单了解一下变更历史表。问题上的变更历史保存了关于变更的关键信息——变更了什么,何时发生。这些信息有时在报告中非常有用,有时我们也会发现自己手动在数据库中添加变更历史,以记录我们通过 SQL 所做的更改——例如,像本章前面提到的通过 SQL 更新问题状态。

在同一时间点上发生的关于某个问题的一组变更会被聚合在一起,形成一个变更组。changegroup表中会有每个变更组的记录,记录了该变更发生在哪个问题、由哪个用户做出的变更,以及变更发生的时间。

然后,changeitem表中会有每个单独变更的记录,所有记录都指向相应的changegroupchangeitem表保存了实际更改的信息——旧值和新值。对于某些字段(如状态),可能有数值和文本表示,文本表示包含人类可读的文本(如status name),以及一个唯一的 ID(如status_id)。它们分别存储在oldvalueoldstring,以及newvaluenewstring字段中。

如何操作...

让我们来看看如何获取变更历史记录和添加变更。对于给定的问题,我们可以通过一个简单的连接查询找到所有在该问题上发生的变更,如下所示:

select  cg.author, cg.created, ci.oldvalue, ci.oldstring, ci.newvalue, ci.newstring  from changegroup cg inner join changeitem ci on cg.id = ci.groupid  where cg.issueid = (select id from jiraissue where pkey = 'DEMO-123') order by cg.created;

修改此操作来筛选出由某个用户或在特定时间段内进行的更改是非常容易的。

现在,让我们快速看一下如何通过数据库在问题上添加一个新变更,如下所示:

  1. 停止 JIRA 服务器。

  2. 连接到 JIRA 数据库。

  3. changegroup表中创建一条记录,包含问题的正确 ID、作者姓名和创建时间。

    insert into changegroup values (12345,10000,'someguy','2011-06-15');
    
    

    确保 ID 值(12345)大于表中的max(ID)

  4. 为此变更组插入一个变更项。让我们以我们之前所做的状态变更为例:

    insert into changeitem values (11111, 12345, 'jira','status','1','Open','6','Closed');
    
  5. 请注意,这里的groupid第 3 步中的 ID 属性相同。第三列保存字段类型,可以是 JIRA 或自定义类型。对于所有标准的 JIRA 字段,如summarystatus等,字段类型为 JIRA。对于自定义字段,字段类型为custom

    对于像status这样的字段,既有文本表示(名称),也有唯一 ID;因此,oldvalueoldstring列都需要填充。同样,newvaluenewstring列也会填充。对于像Summary这样的字段,只需填充oldstringnewstring列。

    此外,请确保id(11111)大于表中的max(id)

  6. 更新 sequence_value_item 表,使其在 seq_id 列中为 ChangeGroupChangeItem 实体存储更大的值。在之前的案例中,我们可以为 ChangeGroup 设置值 12346,为 ChangeItem 设置值 11112。Ofbiz 通常以每批十个的方式分配 ID,因此 SEQ_ID 是下一个可用的 ID,四舍五入到最接近的 10,不过加 1 应该就足够了。

    update sequence_value_item set seq_id = 12346 where seq_name = 'ChangeGroup';
    update sequence_value_item set seq_id = 11112 where seq_name = 'ChangeItem';
    
    

    注意

    每当向任何 JIRA 表中插入新行时,都需要执行此步骤。sequence_value_item 表中的 seq_id 值应该更新为添加新行的实体的值。新的序列值应至少比该实体的 max(id) 大 1。

  7. 提交更改并启动 JIRA。

  8. 通过进入 管理 | 系统 | 索引 来重新索引 JIRA 实例。

第十一章:实用的配方

在本章中,我们将涵盖:

  • 在 JIRA 中编写服务

  • 在 JIRA 中编写定时任务

  • 在 JIRA 中编写监听器

  • 自定义电子邮件内容

  • 在 Webwork 动作中重定向到不同的页面

  • 为用户详情添加自定义行为

  • 在 JIRA 中部署 Servlet

  • 向 Servlet 上下文添加共享参数

  • 编写 ServletContextListener 接口

  • 使用过滤器拦截 JIRA 中的查询

  • 在 JIRA 中添加和导入组件

  • 向 JIRA 添加新模块类型

  • 启用 JIRA 的访问日志

  • 启用 JIRA 的 SQL 日志

  • 在插件中覆盖 JIRA 的默认组件

  • 从电子邮件创建问题和评论

  • Webwork 插件中的国际化

  • 在 v2 插件之间共享公共库

  • 通过直接 HTML 链接操作

介绍

到目前为止,我们已经将这些方法按照常见主题分组为本书中的不同章节。我们已经看到所有重要的主题,但仍然有一些有用的方法和一些插件模块未在前几章中涵盖。

在本章中,我们将介绍一些在前几章中未涉及的 JIRA 强大的插件点和实用技巧。这些方法并非都相关,但它们各自以不同方式都有用。

在 JIRA 中编写服务

一个按时运行的服务是任何 Web 应用程序中非常需要的功能。如果它能通过用户配置的参数进行管理,而且无需重启等操作,那就更为重要。JIRA 提供了一种机制,可以在每次启动后向其添加按时运行的新服务。它允许我们做与 JIRA 相关的事情,也允许我们做与 JIRA 无关的事情。它让我们与第三方应用集成,做出奇迹!

JIRA 中有内置的服务,例如导出服务、POP 服务、邮件服务等。在这个方法中,我们将看到如何在 JIRA 上添加一个自定义服务。

准备工作

使用 Atlassian 插件 SDK 创建一个骨架插件。请注意,可以删除 atlassian-plugin.xml 文件,因为它在服务中并未使用。

如何实现…

与其他 JIRA 插件模块不同,服务不需要插件描述符。它使用的是配置 XML。它通常是一个包含相关类、文件和配置 XML 的 JAR 文件。以下是编写一个简单服务的步骤,该服务仅将内容打印到服务器控制台:

  1. 编写配置 XML。这是服务中最重要的部分。以下是一个简单的配置 XML:

    <someservice id="jtricksserviceid">
      <description>My New Service</description>
      <properties></properties>
    </someservice>
    

    这是一个简单的配置 XML 文件,不包含任何属性。它有一个根元素和一个唯一的 ID,两个都可以使用你选择的自定义名称。我们这里的根元素是 someservice,ID 是 jtricksserviceid。描述如其名所示,只是服务的简短描述。properties 标签包含你希望与服务关联的不同属性。这些属性将在配置服务时由用户输入。稍后我们会更详细地讲解。

  2. 将 XML 文件放到 src/main/resources/com/jtricks/services 目录下。

  3. 创建服务类。该类可以放在任何包结构下,因为它在添加到 JIRA 时将通过完全限定名引用。该类应继承 AbstractService,该类实现了 JTricksService

    public class JTricksService extends AbstractService {
      ...
    }
    
  4. 在服务类中实现必需的方法。以下是你需要实现的唯一方法:

    public void run() {
      System.out.println("Running the JTricks service!!");
    }
    
    public ObjectConfiguration getObjectConfiguration() throws ObjectConfigurationException {
      return getObjectConfiguration("MYNEWSERVICE", "com/jtricks/services/myjtricksservice.xml", null);
    }
    

    这里的 run 是在服务按时运行时执行的关键方法。

    另一个关键的必需方法是 getObjectConfiguration()。我们将在此方法中获取我们之前编写的 XML 文件的配置(见 步骤 1)。在这里我们所需要做的就是调用父类的 getObjectConfiguration 方法,并传入三个参数。第一个参数是一个唯一的 ID(不需要与 XML 文件中的 ID 相同)。此 ID 在内部保存配置时用作键。第二个参数是我们之前编写的配置 XML 文件的 路径,第三个参数是一个 Map,你可以使用它将用户参数添加到对象配置中。

    注意

    对于服务而言,第三个参数通常为 null,因为这些用户参数不会在任何地方使用。它在 JIRA 的其他地方有意义,比如 portlets,但在服务中并不适用。

  5. 将这两个文件编译成 JAR 文件并放到 WEB-INF/lib 目录下。

  6. 重启 JIRA。

现在服务已经准备好。我们可以去 管理 | 系统 | 服务 并添加新的服务,同时设置适当的延迟。在添加服务时,我们需要使用服务类的完全限定名。有关注册服务的更多信息,请参阅:confluence.atlassian.com/display/JIRA/Services#Services-RegisteringaService,这部分内容超出了本书的范围。

参见

  • 服务 添加 可配置 参数

向服务中添加可配置的参数

对于我们刚写的简单服务,只有一个可以配置的参数,那就是服务运行的延迟!如果我们需要添加更多参数怎么办?假设我们想要在服务中添加教程名称,后续如果需要可以更改它。

如何做…

以下是步骤:

  1. 修改服务配置 XML 文件以包含可配置的属性:

    <someservice id="jtricksserviceid">
      <description>My New Service</description>
      <properties>
        <property>
          <key>Tutorial</key>
          <name>The tutorial you like</name>
          <type>string</type>
        </property>
      </properties>
    </someservice>
    

    在这里,我们添加了一个字符串属性,键名是:Tutorial

  2. 在服务类中重写 init() 方法以检索新属性。

    @Override
    public void init(PropertySet props) throws ObjectConfigurationException {
      super.init(props);
      if (hasProperty(TUTORIAL)) {
        tutorial = getProperty(TUTORIAL);
      } else {
        tutorial = "I don't like tutorials!";
      }
    }
    

    在这里,我们在 init 方法中从 PropertySet 中检索 Tutorial 属性。

  3. run() 方法中适当地使用该属性。这里,我们仅打印教程名称:

    @Override
    public void run() {
      System.out.println("Running the JTricks service!! Tutorial? " + tutorial);
    }
    

它是如何工作的…

每当服务被配置或重新配置时,init 方法将被调用,我们在 JIRA 管理界面输入的属性值将在此方法中被检索,用于 run() 方法中。

我们还可以选择性地重写 destroy 方法,在服务被移除之前执行任何操作!

一旦服务被部署并添加到 GUI 中,它会打印出 运行 JTricks 服务 教程? 喜欢 教程! 因为教程属性尚未配置。

它是如何工作的...

前往 管理 | 系统 | 服务 区域,编辑服务并在 喜欢的 教程 字段中输入一个值。假设你输入了 JTricks 教程,你将看到如下输出:

它是如何工作的...

参见

  • 编写 一个 JIRA 中的 服务

在 JIRA 中编写计划任务

你是否曾经想过在 JIRA 中运行计划任务?当我们有了 JIRA 服务时,为什么还需要计划任务?我们在前面的配方中已经看到如何编写一个服务。但尽管我们已经讨论了这些服务的诸多优点,它们也有一个缺点。每次 JIRA 重启时,它都会启动并在之后按规律间隔运行。因此,如果你有一个执行大量内存密集型操作的服务,并且你在一天中的某个时间重启了 JIRA,你会突然发现实例的性能受到影响!如果它被设置为每 24 小时运行一次,你将会发现这个服务从那时起到下次重启前会在白天中运行。

JIRA 中的计划任务是确保所有此类操作在较为空闲的时段进行的好方法,例如在午夜。在本章中,我们将编写一个简单的计划任务,看看它是多么容易!

如何实现…

让我们编写一个简单的计划任务,在控制台中打印一行。以下是步骤:

  1. 编写一个实现 Quartz job 接口的 Java 类。JIRA 内部使用 Quartz 来调度任务,因此 Quartz 被捆绑在 JIRA 中。

    public class JTricksScheduledJob implements Job{
      ...
    }
    
  2. 实现 execute 方法。这个方法每次作业运行时都会被执行。我们在此方法中做的任何事情,可以像一行代码一样简单,也可以像启动核爆一样复杂!我们的计划任务只是打印一行到控制台,因此我们编写的 Java 类如下所示:

    public void execute(JobExecutionContext context) throws JobExecutionException {
      System.out.println("Running the job at "+(new Date()).toString());
    }
    
  3. 将类打包成 JAR 文件并部署到 WEB-INF/lib 文件夹下。

  4. 修改 WEB-INF/classes 文件夹中的 scheduler-config.xml 文件,以让 JIRA 知道我们的新计划任务。JIRA 将所有关于计划任务的信息存储在此文件中:

    1. <job> 标签下定义一个作业,如下所示:

      <job name="JTricksJob" class="com.jtricks.JTricksScheduledJob" />
      
    2. 添加一个触发器来运行JTricksJob。在这里,我们定义cron表达式来在指定的时间运行任务:

      <trigger name="JTricksJobTrigger" job="JTricksJob" type="cron">
        <expression>0 0/2 * * * ?</expression><!-- run every 2 minutes -->
      </trigger>
      
    3. 上一个触发器将任务安排为每两分钟运行一次。关于编写cron表达式的更多详细信息可以参见:www.quartz-scheduler.org/docs/tutorial/TutorialLesson06.html

  5. 重启 JIRA。

工作原理...

一旦 JIRA 重启,新的任务可以在管理 | 系统 | 调度器 详细信息页面下看到。我们还可以在同一页面验证任务的下一次触发时间,如下图所示:

工作原理...

当任务运行时,您将在控制台看到以下内容!

工作原理...

在 JIRA 中编写监听器

监听器是 JIRA 中非常强大的功能。JIRA 有一个机制,当某些操作发生在问题上时,就会抛出事件,例如创建问题、更新问题、工作流进展等类似事件。通过使用监听器,我们可以捕捉这些事件,并根据我们的需求执行特定的操作。

实现监听器在 JIRA 中有两种不同的方法。旧的方法是扩展AbstractIssueEventListener类,该类又实现了IssueEventListener接口。AbstractIssueEventListener类捕获事件,识别其类型,并将事件委派给适当的方法进行处理。要编写新的监听器,我们需要做的就是扩展AbstractIssueEventListener类,并重写我们感兴趣的方法!

新的做法是使用atlassian-event库。在这里,我们在插件描述符中注册监听器,并借助@EventListener注解实现监听器。

目前,JIRA 支持两种方式,尽管它们各自有优缺点。例如,旧方法可以为监听器添加属性,而新方法不支持添加属性,但新方法不需要任何配置,因为它会自动注册。另一方面,新方法可以作为一个完全成熟的 v2.0 插件来编写。

在本教程中,我们将看到如何使用这两种方法编写监听器。

准备工作

使用 Atlassian Plugin SDK 创建一个骨架插件。

如何操作...

通过扩展AbstractIssueEventListener来编写旧方法中的监听器,如下所示:

  1. 创建一个继承AbstractIssueEventListener类的监听器类。

    public class OldEventListener extends AbstractIssueEventListener {
      ...
    }
    
  2. 定义监听器的属性。这是一个可选步骤,只有当您需要为监听器定义属性时才需要,这些属性可以在执行过程中使用。一个示例是输入邮件服务器的详细信息,如果我们有一个监听器,当事件被触发时,它会使用特定的邮件服务器发送自定义电子邮件。

    1. 重写getAcceptedParams方法,返回要定义的属性的字符串数组。

      @Override
      public String[] getAcceptedParams() {
        return new String[] { "prop 1" };
      }
      
    2. 在这里,我们定义了一个名为prop1的属性。

    3. 重写init方法,并检索用户输入的属性值。

      @Override
      public void init(Map params) {
        prop1 = (String) params.get("prop 1");
      }
      
    4. 每当配置或重新配置监听器时,init方法都会被调用。在这里,我们只是检索属性值并将其分配给类变量以供将来使用。

  3. 重写适当的监听器方法。例如,可以通过重写issueCreated方法来捕获问题创建事件,如下所示。

    @Override
    public void issueCreated(IssueEvent event) {
      Issue issue = event.getIssue();
      System.out.println("Issue " + issue.getKey() + " has been created and property is:"+prop1);
    }
    

    这里,我们只是获取触发事件的问题——在这种情况下是新创建的问题——并打印出相关细节以及监听器属性。我们还可以在此方法中编写更复杂的方法。如果事件涉及更改,例如issueUpdated事件或在使用过渡时输入评论时,还可以从事件中获取其他内容,如更改日志详情。

    注意

    请注意,只有少数几个事件可以以这种方式进行监听,有些事件,比如project creation,根本不会触发事件!在这种情况下,您可能需要扩展相应的操作,并在需要时抛出自定义事件。所有可用事件可以在以下网址找到:docs.atlassian.com/jira/latest/com/atlassian/jira/event/issue/IssueEventListener.html

  4. 这里值得提到的一个重要方法是customEvent方法,每当触发自定义事件时,该方法都会被调用。这适用于用户配置的所有自定义事件,如下一个例子所述。我们可以按如下方式捕获它们:

    @Override
    public void customEvent(IssueEvent event) {
      Long eventTypeId = event.getEventTypeId();
      Issue issue = event.getIssue();
      if (eventTypeId.equals(10033L)) {
        System.out.println("Custom Event thrown here for issue:" + issue.getKey()+" and property is:"+prop1);
      }
    }
    

    这里,10033 是新事件的 ID。

  5. 将类打包成 JAR 文件,并部署到jira-home/plugins/installed-plugins文件夹下。

  6. 重启 JIRA

  7. 通过转到管理 | 系统 | 监听器来配置监听器。

    1. 输入名称和完整限定类名,然后点击添加

    2. 编辑监听器,添加属性(如果有的话)!

以新方式创建的监听器,即使用@EventListener注解,写法如下:

  1. atlassian-plugin.xml中注册监听器。

    <component key="eventListener" class="com.jtricks.NewEventListener">
      <description>Class that processes the new JIRA Event</description>
    </component>
    

    这里,class属性保存的是我们将要编写的监听器类的完整限定名。

  2. 使用component-import插件模块导入EventPublisher组件。

    <component-import key="eventPublisher" interface="com.atlassian.event.api.EventPublisher"/>
    
  3. 编写监听器类:

    1. EventPublisher组件注入类中,并使用register方法进行自注册,如下所示:

      public class NewEventListener {
        public NewEventListener(EventPublisher eventPublisher) {
          eventPublisher.register(this);
        }
      }
      
    2. 创建方法来处理事件,使用@EventListener,如下代码所示:

      @EventListener
      public void onIssueEvent(IssueEvent issueEvent) {  
      System.out.println("Capturing event with 
      ID:"+issueEvent.getEventTypeId()+" here");
        ...
      }
      

      请注意,该注解可以与类中的任意数量的公共方法一起使用,并且在 JIRA 中触发事件时,所有这些方法都会被调用。

    3. 适当处理事件。

      @EventListener
      public void onIssueEvent(IssueEvent issueEvent) {  System.out.println("Capturing event with ID:"+issueEvent.getEventTypeId()+" here");
        Long eventTypeId = issueEvent.getEventTypeId();
             Issue issue = issueEvent.getIssue();
      
        if (eventTypeId.equals(EventType.ISSUE_CREATED_ID)) {
          System.out.println("Issue "+issue.getKey()+" has been created");
              } else if (eventTypeId.equals(10033L)) {
                System.out.println("Custom Event thrown here for issue:"+issue.getKey());
              }
      }
      

    如我们所见,事件 ID 会被检查并适当处理。首先,我们处理了问题创建事件,然后是自定义事件。

  4. 打包插件并将其部署到jira-home/plugins/installed-plugins文件夹中。

工作原理...

在这两种情况下,配置完成后,监听器的工作方式完全相同。请注意,配置仅适用于旧的方式,配置完成后,监听器可以在管理 | 系统 | 监听器下看到,如下所示:

工作原理...

请注意,监听器中配置了属性prop 1

当事件在 JIRA 中触发时,监听器捕获事件并调用适当的方法。旧的方法会打印问题关键字和属性名称。新方法也以相同的方式工作,唯一不同的是没有属性值。

工作原理...

即使是采用新的方式,也可以向监听器添加属性,但这需要一个单独的配置屏幕来捕获和维护这些属性。

还有更多...

插件可能会在服务仍然运行时被管理员禁用和重新启用。构造函数在监听器初次加载时会在 JIRA 启动时调用,但我们可能希望单独处理插件的启用或禁用,因为这些操作不会在构造函数中捕获。

插件的处理、启用和禁用

Atlassian 插件作为 Spring 动态模块实现,atlassian-plugin.xml 在加载之前会被转换为 Spring XML bean 配置。对于监听器,事件监听器将变成 Spring bean,因此我们可以应用 Spring 接口——InitializingBeanDisposableBean——来捕获 bean 的创建和销毁。在我们的案例中,代码修改如下:

public class NewEventListenerModified implements InitializingBean, DisposableBean {
  private final EventPublisher eventPublisher;
  public NewEventListenerModified(EventPublisher eventPublisher) {
    this.eventPublisher = eventPublisher;
  }
  @EventListener
  public void onIssueEvent(IssueEvent issueEvent) {
    System.out.println("Capturing event with ID:" + issueEvent.getEventTypeId() + " here");
    Long eventTypeId = issueEvent.getEventTypeId();
    Issue issue = issueEvent.getIssue();
    if (eventTypeId.equals(EventType.ISSUE_CREATED_ID)) {
      System.out.println("Issue " + issue.getKey() + " has been created");
    } else if (eventTypeId.equals(10033L)) {
      System.out.println("Custom Event thrown here for issue:" + issue.getKey());
    }
  }
  public void afterPropertiesSet() throws Exception {
    eventPublisher.register(this);

  public void destroy() throws Exception {
    eventPublisher.unregister(this);
  }
}

如您所见,注册和注销分别发生在方法afterPropertiesSetdestroy事件中。这些方法在 bean 的创建/销毁过程中被调用,从而有效地处理插件的启用/禁用。

别忘了在项目构建路径中添加spring-beans JAR,以避免编译问题!另外,也可以在pom.xml中添加以下依赖项:

<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>2.5.6</version>
            <scope>provided</scope>
</dependency>

自定义电子邮件内容

我们已经看到,当某些事情发生时,JIRA 如何触发各种事件,以及我们如何处理这些事件以执行自定义操作。处理这些事件的一种方式是根据 JIRA 中设置的通知方案向用户发送电子邮件通知。但如果我们不喜欢 JIRA 通知的默认内容呢?如果我们只是想更改措辞,甚至修改电子邮件内容呢?

在本示例中,我们将展示如何自定义在 JIRA 中触发事件时发送的电子邮件通知内容。

如何实现...

JIRA 有一套使用 Velocity 编写的电子邮件模板,当通知发送时会被渲染。对于每个事件,JIRA 中都有一个配置的模板,当事件触发时会使用该模板。我们可以创建新的模板并编辑事件以使用这些新模板,或者修改现有模板并保持事件不变!

在这两种情况下,步骤基本相似,如下所示:

  1. 找到需要更改通知的事件。该事件可以是现有的 JIRA 事件,如Issue CreatedIssue Updated,也可以是 JIRA 管理员创建的自定义事件。

  2. 查找与事件映射的模板。

    对于每个事件,无论是系统事件还是自定义事件,都有一个关联的模板。我们无法更改与系统事件关联的模板。例如,Issue Updated事件关联的是Issue Updated模板。然而,我们可以为自己添加的自定义事件选择任何模板。

  3. 选择的模板的电子邮件模板映射可以在atlassian-jira/WEB-INF/classes/email-template-id-mappings.xml中找到。在该文件中,我们可以找到与每个事件相关的多个模板。例如,Issue Updated事件有如下条目:

    <templatemappings>
      ...
      <templatemapping id="2">
        <name>Issue Updated</name>
        <template>issueupdated.vm</template>
        <templatetype>issueevent</templatetype>
      </templatemapping>
      ...
    </templatemappings>
    
  4. 在这里,如果我们要添加新模板,可以按如下方式添加新的映射:

    <templatemappings>
      ...
      <templatemapping id="20">
        <name>Demo Event</name>
        <template>demoevent.vm</template>
        <templatetype>issueevent</templatetype>
      </templatemapping>
      ...
    </templatemappings>
    

    确保我们在这里使用的id在文件中是唯一的。

  5. 如果我们要自定义现有模板或添加一个新模板,首先需要在email-template-id-mappings.xml文件中找到要编辑的模板,或者按照文件中提到的名称添加新模板。

    电子邮件模板存储在 JIRA 的两个不同位置,一个用于 HTML 邮件,另一个用于文本邮件。它们的模板可以分别在WEB-INF/classes/templates/email/htmlWEB-INF/classes/templates/email/text找到。此外,电子邮件的主题可以在WEB-INF/classes/templates/email/subject中找到。

    请注意,模板的名称在所有三个位置都是相同的。在我们的示例中,要编辑的模板名称是issueupdated.vm,因此如果我们仅需要修改主题,只需修改WEB-INF/classes/templates/email/subject/issueupdated.vm文件。同样,HTML 或文本内容可以分别在WEB-INF/classes/templates/email/html/issueupdated.vmWEB-INF/classes/templates/email/text/issueupdated.vm中进行编辑。

    如果我们要添加模板,例如在我们的例子中是demoevent.vm,我们需要创建三个模板,每个模板分别用于主题、HTML 正文和文本正文,并将它们放入相应的文件夹中。

  6. 编辑完模板后,重新启动 JIRA。

它是如何工作的...

在添加新模板并重启 JIRA 后,我们可以将其与我们创建的自定义事件关联。当通知发送时,JIRA 将使用更新或新添加的模板来呈现电子邮件内容。

还有更多...

通过编辑相关的 Velocity 模板,您可以在通知电子邮件中添加有关问题的更多信息,如自定义字段。

高级自定义——添加自定义字段信息

所有的 VM 模板在 Velocity 上下文中都包含了$issue对象,以及其他在以下地址详细说明的变量:confluence.atlassian.com/display/JIRADEV/Velocity+Context+for+Email+Templates。在生成电子邮件内容时,使用它来检索问题的内容非常容易。

例如,$issue.summary将检索问题摘要,您可以在通过WEB-INF/classes/templates/email/subject/issueupdated.vm渲染的电子邮件主题中看到它。类似地,其他问题信息也可以轻松访问。例如,自定义字段的详细信息可以通过以下方式访问:

$issue.getCustomFieldValue($customFieldManager.getCustomFieldObject("customfield_10010"))

这里的10010是自定义字段的唯一 ID。

您可以在以下地址查看其他格式化的示例:confluence.atlassian.com/display/JIRADEV/Adding+Custom+Fields+to+Email

在 Webwork 操作中重定向到不同的页面

本食谱涵盖了 JIRA Web 操作中一个非常简单的概念。在编写插件时,我们经常会遇到需要在执行操作后导航到新页面的场景,例如仪表板、浏览新项目或查看其他问题。JiraWebActionSupport提供了一种简单的方法来实现这一点,我们将在本食谱中看到这一点。

如何操作...

如果我们希望在执行操作时导航到仪表板,而不是渲染成功视图该怎么办?如果我们无法直接从 JSP 页面或 Velocity 模板中链接它,因为我们想在重定向之前在操作类中执行某些操作该怎么办?

您需要做的就是在操作类的doExecute方法(或适当的方法)中返回getRedirect(URL)!当操作方法成功完成时,这个方法会重定向到指定位置。如果出现任何错误,它将跳转到错误页面,因为getRedirect()方法在这种情况下返回Action.ERROR

即使存在错误,您也可以通过使用forceRedirect(URL)方法而不是getRedirect()方法强制重定向到指定 URL。它不会清除返回的 URL,始终会跳转到重定向 URL。

例如,如果我们在成功时需要返回到仪表板,我们可以这样做:

@Override
protected String doExecute() throws Exception {
  System.out.println("Action invoked. Doing something important before redirecting to Dashboard!");
  return getRedirect("/secure/Dashboard.jspa");
}

getRedirect替换为forceRedirect将使用户无论结果如何都跳转到仪表板。

为用户详情添加自定义行为

在 JIRA 中,您可以看到用户详情是通过全名和指向用户在应用程序中个人资料的链接来格式化的。例如,当问题在问题导航器中显示时,指派人和报告人会显示如下:

为用户详情添加自定义行为

但是,如果我们想改变用户详细信息的显示方式呢?比如,如果我们想展示用户头像?或者,如果我们想通过外部链接展示他们的用户名,例如指向他们 Twitter 个人资料的链接?

JIRA 提供了用户格式插件模块来实现此目的。使用此模块,我们可以定义不同的格式,在这些格式中,用户将以特定的方式显示,并可在现有的 JIRA 展示或我们自定义的插件中使用它们。

准备工作

使用 Atlassian 插件 SDK 创建一个骨架插件。

如何实现...

在本教程中,让我们尝试创建一个新的用户配置文件,将显示用户名(而不是全名)并提供指向他们 Twitter 个人资料的链接,增加一些趣味!以下是实现步骤:

  1. 将用户配置文件模块添加到atlassian-plugin.xml

    <user-format key="twitter-format" name="Twitter User Format" class="com.jtricks.TwitterUserFormat" system="true">
      <description>User name linking to twitter</description>   <type>twitterLink</type>
      <resource type="velocity" name="view" location="templates/twitterLink.vm"/>
    </user-format>
    

    与其他插件模块一样,用户配置文件模块也有一个唯一的key。然后,它指向将由用户格式化程序使用的class,在此情况下是TwitterUserFormat

    type元素包含将在格式化用户时使用的唯一配置文件类型名称。以下是 JIRA 4.4 版本中默认存在的类型:profileLinkfullNameprofileLinkSearcherprofileLinkExternalprofileLinkActionHeaderfullProfile

    resource元素指向用于渲染视图的 velocity 模板,在此情况下是twitterLink.vm

  2. 在上一步中创建格式化器类。该类应实现UserFormat接口。

    public class TwitterUserFormat implements UserFormat {
      private final UserFormatModuleDescriptor moduleDescriptor;
    
      public TwitterUserFormat(UserFormatModuleDescriptor moduleDescriptor){
        this.moduleDescriptor = moduleDescriptor;
      }
    }
    

    在这里,我们将UserFormatModuleDescriptor注入类中,因为它将在渲染 velocity 模板时使用,正如下一步所示。

  3. 实现所需的方法。我们需要实现两个重写的format方法。

    第一个方法接受usernameid,其中username是用户的名字,它也可以是null,而id是一个额外的参数,可以用来向渲染器传递额外的上下文。理想情况下,某个实现可能会在渲染的输出中包含该 ID,以便可以用于测试断言。ID 的使用示例如显示分配人(/WEB-INF/classes/templates/jira/issue/field/assignee-columnview.vm)中,ID 是分配人。

    我们在示例中不使用 ID,但该方法实现如下:

    public String format(String username, String id) {
      final Map<String, Object> params = getInitialParams(username, id);
      return moduleDescriptor.getHtml(VIEW_TEMPLATE, params);
    }
    

    getInitialParams仅用用户名填充params映射,如下所示:

    private Map<String, Object> getInitialParams(final String username, final String id) {
      final Map<String, Object> params = MapBuilder.<String, Object> newBuilder().add("username", username).toMutableMap();
      return params;
    }
    

    如果我们希望以其他方式渲染用户详细信息,我们可以根据需要在映射中填充任意数量的内容!

    第二个方法接受usernameid和一个预先填充了额外值的map,用于向上下文添加更多内容!该方法实现如下:

    public String format(String username, String id, Map<String, Object> params) {  
      final Map<String, Object> velocityParams = 
      getInitialParams(username, id);  
      velocityParams.putAll(params);
      return moduleDescriptor.getHtml(VIEW_TEMPLATE, velocityParams);
    }
    

    唯一的区别是,额外的上下文也被填充到params映射中。

    在这两种情况下,moduleDescriptor将渲染 velocity 模板,该模板由VIEW_TEMPLATE或“view”名称定义。

  4. 编写 velocity 模板,使用上一步中 params 映射中填充的上下文来显示用户信息:

    #if ($username)
      #set ($quote = '"')
      #set($author = "<a id=${quote}${textutils.htmlEncode($username)}${quote} href=${quote}http://twitter.com/#!/${username}${quote}>$textutils.htmlEncode($username)</a>")
    #else
        #set($author = $i18n.getText('common.words.anonymous'))
    #end
    ${author}
    

    在我们的示例中,我们只显示用户名,并提供一个链接 http://twitter.com/#!/${username},该链接指向拥有该用户名的 Twitter 账户。注意,quote 变量在单引号内赋值为双引号。单引号是 velocity 语法,而双引号是值。它用于构建 URL,其中编码的名称、href 值等都放在引号之间!

    别忘了处理用户为 null 的情况。在我们的示例中,当用户为 null 时,我们只显示“匿名”作为名称。

  5. 打包插件并部署。

工作原理...

插件部署后,新的用户资料(在此例中为 twitterLink)可以在 JIRA 中的各个适当位置使用。例如,assignee-columnview.vm 可以修改为包含 twitterLink 资料,而不是默认的 profileLink,如下所示:

#if($assigneeUsername)
    #if ($displayParams && $displayParams.nolink)
        $userformat.formatUser($assigneeUsername, 'fullName', 'assignee')
    #else
        <span class="tinylink">$userformat.formatUser($assigneeUsername, 'twitterLink', 'assignee')</span>
    #end
#else
    <em>$i18n.getText('common.concepts.unassigned')</em>
#end

当你这样做时,问题导航器中的指派人列将显示如下,并包含指向用户 Twitter 账户的链接:

工作原理...

我们还可以在插件中使用新的资料,通过调用 formatUser 来呈现用户详细信息,如下所示:

$userformat.formatUser($username, 'twitterLink', 'some_id')

或:

$userformat.formatUser($username, 'twitterLink', 'some_id', $someMapWithExtraContext)

在 JIRA 中部署 servlet

我们都知道 servlet 有多么有用!JIRA 提供了一种简便的方式,通过 Servlet 插件模块来部署 JAVA servlet。在本教程中,我们将看到如何编写一个简单的 servlet 并在 JIRA 中访问它。

准备工作

使用 Atlassian Plugin SDK 创建一个插件骨架。

如何做...

以下是在 JIRA 中部署 JAVA servlet 的步骤:

  1. 将 servlet 插件模块包含在 atlassian-plugin.xml 中。Servlet 插件模块支持以下一组属性:

    1. class: 它是 servlet 的 Java 类,必须是 javax.servlet.http.HttpServlet 的子类。此属性是必需的。

    2. disabled: 它表示插件模块是否默认禁用或启用。默认情况下,模块是启用的。

    3. i18n-name-key: 插件模块人类可读名称的本地化键。

    4. key: 它表示插件模块的唯一键。此属性是必需的。

    5. name: 它是 servlet 的人类可读名称。

    6. system: 它表示该插件模块是否为系统插件模块。仅适用于非 OSGi 插件。

    以下是支持的子元素:

    1. description: 插件模块的描述。

    2. init-param: servlet 的初始化参数,通过 param-nameparam-value 子元素指定,方式与 web.xml 相同。此元素及其子元素可以重复。

    3. resource: 此插件模块的资源。该元素可以重复。

    4. url-pattern: 要匹配的 URL 模式。此元素是必需的,并且可以重复。

    在我们的示例中,我们只使用必填字段和一些示例的 init-params,如下所示:

    <servlet name="Test Servlet" key="jtricksServlet" class="com.jtricks.JTricksServlet">
        <description>Test Servlet</description>
        <url-pattern>/myWebsite</url-pattern>
        <init-param>
            <param-name>siteName</param-name>
            <param-value>Atlassian</param-value>
        </init-param>
        <init-param>
            <param-name>siteAddress</param-name>
            <param-value>http://www.atlassian.com/</param-value>
        </init-param>
    </servlet>
    

    这里 JTricksServlet 是 servlet 类,而 /myWebsite 是 URL 模式。我们还传递了一些 init 参数:siteNamesiteAddress

  2. 创建一个 servlet 类。该类必须继承 javax.servlet.http.HttpServlet

    public class JTricksServlet extends HttpServlet {
      ...
    }
    
  3. 实现必要的方法:

    1. 我们可以在 init 方法中检索 init 参数,如下所示:

      @Override
      public void init(ServletConfig config) throws ServletException {
        super.init(config);
        authenticationContext = ComponentManager.getInstance().getJiraAuthenticationContext();  siteName = config.getInitParameter("siteName");
        siteAddress = config.getInitParameter("siteAddress");
      }
      

      每次初始化 servlet 时都会调用 init() 方法,这发生在 servlet 第一次被访问时。当插件模块被禁用并重新启用时,init() 方法也会在 servlet 第一次被访问时被调用。

      如您所见,我们在 servlet 插件模块中定义的 init 参数可以从 ServletConfig 中访问。此处,我们还初始化了 JiraAuthenticationContext,以便可以用它在 servlet 中检索当前登录用户的详细信息。类似地,我们也可以在这里初始化任何 JIRA 组件。

    2. 实现 doGet() 和/或 doPost() 方法,执行所需的功能。对于本示例,我们将仅使用 init 参数创建一个简单的 HTML 页面,并将一行输出到控制台。

      @Override
      protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
        resp.setContentType("text/html");
        PrintWriter out = resp.getWriter();
      
        User user = authenticationContext.getUser();  out.println("Welcome " + (user != null ? user.getFullName() : "Anonymous"));
        out.println("<br>Invoking the servlet...");
        out.println("<br>My Website : <a href=\"" + siteAddress + "\">" + siteName + "</a>");
      
        doSomething();
      
        out.println("<br>Done!");
      }
      
      private void doSomething() {
        System.out.println("Invoked servlet at " + (new Date()));
      }
      

      authenticationContext 如前所述检索当前用户名。从 JIRA 4.3 开始,getLoggedInUser() 方法用于检索当前用户,getDisplayName() 获取用户的全名。

  4. 打包插件并部署。

如何工作...

一旦部署,servlet 将可以通过以下 URL 访问:http://yourserver/jira/plugins/servlet/${urlPattern}。在我们的例子中,URL 是 http://yourserver/jira/plugins/servlet/myWebsite

当 servlet 在 /plugins/servlet/myWebsite 被访问时,输出如以下截图所示:

如何工作...

向 Servlet 上下文添加共享参数

在上一个示例中,我们展示了如何部署一个 servlet 并如何使用 init 参数。如果我们有一组 servlets、servlet filters 或 context listeners 使用相同的参数,是否真的需要在所有插件模块中初始化它们?

在这个示例中,我们将展示如何使用 Servlet 上下文参数插件模块在多个 servlets、filters 和 listeners 之间共享参数。

准备工作

使用 Atlassian 插件 SDK 创建一个骨架插件。

操作步骤...

我们需要做的就是定义共享参数,为每个共享参数在 atlassian-plugin.xml 中添加一个 servlet-context-param 模块。

例如,可以如下定义一个键为 sharedText 的参数:

<servlet-context-param key="jtricksContext">
  <description>Shares this param!</description>
  <param-name>sharedText</param-name>
  <param-value>This is a shared Text</param-value>
</servlet-context-param> 

确保模块具有唯一的键。这里,参数名称是 sharedText,其值为 This is a shared Text。一旦插件被打包并部署,参数 sharedText 将在 servlets、filters 和 listeners 之间共享。

在 servlet 中,我们可以在 init 方法中如下访问该参数:

@Override
public void init(ServletConfig config) throws ServletException {
  super.init(config);
  String sharedText = config.getServletContext().getInitParameter("sharedText");
}

如何工作...

一旦获取到共享文本,我们可以在任何地方使用它,比如在构建 HTML 时。

out.println("<br>Shared Text:"+sharedText);

现在,servlet 也将打印出来,如下图所示:

它是如何工作的...

编写 Servlet 上下文监听器

我们已经了解了如何编写 servlets。那么,如何为相同的应用编写一个上下文监听器呢?如果你想与使用上下文监听器进行初始化的框架集成,这会非常有用。

准备就绪

使用 Atlassian Plugin SDK 创建一个骨架插件。

如何实现...

以下是编写简单上下文监听器的步骤:

  1. atlassian-plugin.xml 中包含 servlet-context-listener 模块。

    <servlet-context-listener name="Test Servlet Listener" key="jtricksServletListener" class="com.jtricks.JTricksServletListener">
      <description>Listener for Test Servlet</description>
    </servlet-context-listener>
    

    在这里,我们有一个独特的模块键和一个类,该类是 servlet 上下文监听器的 Java 类。

  2. 编写 servlet 上下文监听器的类。该类必须实现 javax.servlet.ServletContextListener

    public class JTricksServletListener implements ServletContextListener{	  ...
    }
    
  3. 根据需要实现上下文监听器方法。例如,我们只是将一些语句打印到控制台:

    public void contextDestroyed(ServletContextEvent event) {  System.out.println("Test Servlet Context is destroyed!");
    }
    
    public void contextInitialized(ServletContextEvent event) {  System.out.println("Test Servlet Context is initialized!");
    }
    

    可以从 ServletContextEvent 对象中找到已初始化或销毁的上下文的详细信息。

  4. 打包插件并部署。

它是如何工作的...

contextInitialized 方法不会在应用程序启动时调用。相反,每次启用插件后,只有当访问插件中的 servlet 或过滤器时,它才会第一次被调用。

它是如何工作的...

类似地,contextDestroyed 方法会在每次禁用包含 servlet 或过滤器的插件模块时被调用。

使用过滤器拦截 JIRA 查询

Servlet 过滤器提供了一个强大的机制,可以拦截查询并执行一些智能操作,如分析、监控、内容生成等。它的工作原理与任何正常的 Java servlet 过滤器一样,JIRA 提供了 Servlet Filter Plugin Module 来通过插件添加它们。在这个教程中,我们将学习如何使用过滤器拦截某些 JIRA 查询以及如何利用它们!

与其他 servlet 插件模块一样,servlet-filter plugin 模块也有一个唯一的 key 和一个与之关联的 classname 属性保存过滤器的可读名称,weight 指示过滤器在过滤器链中的顺序。权重越高,过滤器的位置越低。

另一个重要的属性是 location,它表示过滤器在应用程序过滤器链中的位置。以下是位置的四个可能值:

  • after-encoding:在应用程序中过滤器链的最顶部,但在任何确保请求完整性的过滤器之后。

  • before-login:在进行用户登录的过滤器之前。

  • before-decoration:在进行 Sitemesh 装饰响应的过滤器之前。

  • before-dispatch:在过滤器链的末端,在任何默认处理请求的 servlet 或过滤器之前。

weight 属性与 location 一起使用。如果两个过滤器具有相同的位置,则根据 weight 属性的值对它们进行排序。

init-param像往常一样用于接收过滤器的初始化参数。

url-pattern定义了匹配的 URL 模式。此元素可以重复,过滤器将对所有与指定模式匹配的 URL 进行调用。与 servlet URL 不同,url-pattern在此处匹配${baseUrl}/${url-pattern}。该模式可以使用通配符*?,前者匹配零个或多个字符,包括目录斜杠,后者匹配零个或一个字符。

dispatcher是另一个确定何时调用过滤器的元素。你可以包含多个调度程序元素,值为REQUESTINCLUDEFORWARDERROR。如果没有指定,过滤器将在所有情况下调用。

准备工作

使用 Atlassian 插件 SDK 创建一个骨架插件。

如何操作...

让我们尝试拦截所有问题视图,其 URL 格式为${baseUrl}/browse/*-*并记录它们。以下是编写过滤器并实现给定逻辑的逐步过程。

  1. Servlet Filter插件模块添加到atlassian-plugin.xml中。

    <servlet-filter name="Browse Issue Filter" key="jtricksServletFilter" class="com.jtricks.JTricksServletFilter" location="before-dispatch" weight="200">
            <description>Filter for Browse Issue</description>
            <url-pattern>/browse/*-*</url-pattern>
            <init-param>
                <param-name>filterName</param-name>
                <param-value>JTricks Filter</param-value>
            </init-param>
    </servlet-filter>
    

    这里JTricksServletFilter是过滤器类,我们在分发之前添加了该过滤器。在我们的示例中,url-pattern将是/browse/*-*,因为浏览问题的 URL 是${baseUrl}/browse/*-*格式。我们可以根据需要在我们的上下文中使用不同的 URL 模式。

  2. 创建Filter类。该类应实现javax.servlet.Filter

    public class JTricksServletFilter implements Filter {
      ...
    }
    
  3. 实现适当的过滤器方法:

    public void destroy() {
      System.out.println("Filter destroyed!");
    }
    
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
      HttpServletRequest request = (HttpServletRequest) req;
    
      // Get the IP address of client machine.
      String ipAddress = request.getRemoteAddr();
    
      // Log the user details, IP address , current timestamp and URL.  
    System.out.println("Intercepted in filter, request by user:" + authenticationContext.getUser().getFullName()  + " from IP " + ipAddress + " at " + new Date().toString() + ". Accessed URL:"+request.getRequestURI());
    
      chain.doFilter(req, res);
    }
    
    public void init(FilterConfig config) throws ServletException {  System.out.println("Initiating the filter:"+config.getInitParameter("filterName"));
      authenticationContext = ComponentManager.getInstance().getJiraAuthenticationContext();
    }
    

    这里,init方法在初始化过滤器时调用,即插件启用后第一次访问时。在此方法中,我们可以检索定义的init-param实例或通过 Servlet Context Parameter 插件模块定义的参数。在之前的代码片段中,使用getLoggedInUser()来检索 JIRA 4.3+中的已登录用户,并使用getDisplayName()来检索该用户的全名。

    destroy方法在每次销毁过滤器时调用。

    doFilter是每次 URL 与url-pattern匹配时调用的方法。在这里,我们只是打印请求查看问题页面的 IP 地址和用户详细信息,并记录时间,但我们可以做很多事情,比如记录日志、使用数据进行分析或监控等等。

  4. 打包插件并部署。

工作原理...

每当 JIRA 中的 URL 与url-pattern匹配时,相关过滤器会被调用。当你希望在 JIRA 执行特定操作时做一些特定的事情,或者想要监控谁在做什么以及何时做某事,或者基于特定 URL 执行其他操作时,这将非常有帮助。

工作原理...

在我们的示例代码中,每当查看问题时,都会打印详细信息,如前面的截图所示。

在 JIRA 中添加和导入组件

JIRA 有一个组件系统,包含许多注册在 PicoContainer 中的服务类和管理类,它们可以被核心类和插件使用。有时候,将自定义组件添加到这个组件系统中是有意义的,这样可以在同一个插件中的其他模块中使用,或者与其他插件共享。

在本食谱中,我们将看到如何在 JIRA 中添加一个新组件,以及如何在插件内和从单独插件中使用它。

准备工作

使用 Atlassian 插件 SDK 创建一个骨架插件。对于本示例,我们将使用在之前食谱中使用的RedirectAction webwork 模块。

如何操作...

首先,让我们看看如何定义一个组件并在同一个插件中的不同模块中使用它。在我们的示例中,我们将定义一个示例组件,并在RedirectAction中使用它暴露的方法。以下是步骤:

  1. 创建一个具有所需方法定义的接口。组件在其他地方使用时将暴露这些方法:

    package com.jtricks.provider;
    
    public interface MyComponent {
      public void doSomething();
    }
    
  2. 创建实现类并实现方法。

    public class MyComponentImpl implements MyComponent{  
      private final JiraAuthenticationContext authenticationContext;
    
      public MyComponentImpl(JiraAuthenticationContext authenticationContext) {
        this.authenticationContext = authenticationContext;
      }
    
      public void doSomething() {
        System.out.println("Hey "+authenticationContext.getUser().getFullName()+",  Sample method to check Components");
      }
    }
    

    在实现类中,我们可以像往常一样注入 JIRA 组件,并将其用于各种操作。在这里,我们注入JiraAuthenticationContext来获取当前用户的详细信息,仅仅为了打印一条个性化消息!

  3. 使用组件插件模块在atlassian-plugin.xml文件中声明该组件。

    <component key="myComponent" name="My Component" class="com.jtricks.provider.MyComponentImpl">  
      <interface>com.jtricks.provider.MyComponent</interface>
    </component>
    

    在这里,组件模块有一个唯一的键和一个指向实现类的类属性。元素接口指向我们在步骤 1中创建的组件接口。

我们的组件现在已经准备好,并可以在其他插件模块中使用。例如,我们可以像之前看到的那样,在RedirectAction类中使用这个组件,具体如下:

public class RedirectAction extends JiraWebActionSupport {
  private final MyComponent myComponent;

  public RedirectAction(MyComponent myComponent) {
    this.myComponent = myComponent;
  }

  @Override
  protected String doExecute() throws Exception {
    System.out.println("Execute the method in component!");
    this.myComponent.doSomething();
    System.out.println("Succesfully executed. Go to dashboard");
    return getRedirect("/secure/Dashboard.jspa");
  }
}

在这里,组件通过构造函数注入,就像我们通常对待 JIRA 组件一样(记住组件本身中的JiraAuthenticationContext!),并在适当的地方调用暴露的方法,在此示例中是doSomething

将组件暴露给其他插件

当我们像之前讨论的那样创建组件时,它们保持私有,仅在插件内可用,尽管我们可以将这些组件暴露给其他插件。

以下是暴露组件所需的两个步骤:

  1. 将组件声明为公共组件。

  2. 导出插件所需的包,以便它们可以被其他插件使用。

以下是详细步骤:

  1. 像之前一样创建接口和实现类。

  2. 使用组件插件模块在atlassian-plugin.xml中声明该组件为公共组件。为此,我们在组件模块中使用public属性,如下所示:

    <component key="myComponent" name="My Component" class="com.jtricks.provider.MyComponentImpl" public="true">
      <interface>com.jtricks.provider.MyComponent</interface>
    </component>
    

使用atlassian-plugin.xmlplugin-info下的bundle-instructions元素导出包。操作如下:

<plugin-info>
  <description>Adding and importing components to JIRA</description>  <version>2.0</version>
  <vendor name="JTricks" url="http://www.j-tricks.com/" />
  <bundle-instructions>
    <Export-Package>com.jtricks.provider</Export-Package>
  </bundle-instructions>
</plugin-info>

请注意,Export-Package 元素导出了 com.jtricks.provider 包,该包包含接口和实现类。有关捆绑包指令的更多信息,请参见:confluence.atlassian.com/display/PLUGINFRAMEWORK/Creating+your+Plugin+Descriptor#CreatingyourPluginDescriptor-{{bundleinstructions}}element

这样,组件现在已经准备好并可供其他插件使用。

导入公共组件

为了在其他插件中使用公共组件,我们需要首先使用 component-import 插件模块导入它们。该模块在 atlassian-plugin.xml 中的配置如下:

<component-import key="myComponent">         <interface>com.jtricks.provider.MyComponent</interface></component-import>

现在,组件就像是在插件内部创建的一样可用。如果我们希望在新插件中使用该组件,RedirectAction 类也将与原插件中的完全相同。

在组件中使用服务属性

也可以为公共组件定义一个属性映射表,然后在与其他插件一起导入组件时使用。它使用 service-properties 元素来定义这些属性,该元素包含名为 entry 的子元素,并具有 key 和 value 属性。例如,字典服务可以定义带有语言作为键的服务属性,如以下代码片段所示:

<component key="dictionaryService" class="com.myapp.DefaultDictionaryService" interface="com.myapp.DictionaryService">
    <description>Provides a dictionary service.</description>
    <service-properties>
        <entry key="language" value="English" />
    </service-properties>
</component>

现在可以在 component-import 模块上使用 filter 属性,只在服务匹配过滤器时导入组件。例如,具有英语语言的字典服务可以如下导入:

<component-import key="dictionaryService" interface="com.myapp.DictionaryService"  filter="(language=English)" />

它是如何工作的...

当一个组件被安装时,它会生成 atlassian-plugins-spring.xml Spring 框架配置文件,将组件插件模块转换为 Spring bean 定义。生成的文件存储在临时插件 JAR 文件中并安装到框架中。如果将 public 属性设置为 'true',该组件将在后台转化为 OSGi 服务,使用 Spring Dynamic 模块来管理其生命周期。

组件导入还会生成 atlassian-plugins-spring.xml Spring 框架配置文件,并使用 Spring 动态模块将导入的插件模块转换为 OSGi 服务引用。导入的组件将其 bean 名称设置为组件导入键。

在这两种情况下,都可以编写自己的 Spring 配置文件,并将其存储在插件 JAR 文件中的 META-INF/spring 文件夹下。

注意

有关组件插件模块和组件导入插件模块的更多详细信息,请参阅 Atlassian 文档:confluence.atlassian.com/display/PLUGINFRAMEWORK/Component+Plugin+Moduleconfluence.atlassian.com/display/JIRADEV/Component+Import+Plugin+Module

向 JIRA 添加新模块类型

到目前为止,我们在 JIRA 中看到过许多有用的插件模块类型。自定义字段模块类型、webwork 模块类型、servlet 模块类型等等。那么,是否可以在 JIRA 中添加一个自定义模块类型,以便之后用于创建不同的模块呢?

JIRA 提供了模块类型插件模块,利用它我们可以动态地向插件框架添加新的模块类型。在本教程中,我们将看到如何添加这种新的插件模块类型并使用它来创建该类型的不同模块。

准备工作

使用 Atlassian 插件 SDK 创建一个骨架插件。

如何操作...

让我们考虑 Atlassian 在其在线文档中使用的相同示例,即创建一个新的字典插件模块,然后可以使用该模块为其他插件或模块提供字典服务。

以下是定义新插件模块类型的步骤:

  1. atlassin-plugin.xml文件中添加模块类型定义。

    <module-type key="dictionary" class="com.jtricks.DictionaryModuleDescriptor" />
    

    在这里,键必须是唯一的,并将在定义该类型模块时作为根元素。该类指向ModuleDescriptor类,当找到这种类型的新插件模块时,该类会被实例化。

    该模块类型的其他有用属性包括name,它保存一个易于理解的名称,i18n-name-key用于保存易于理解名称的本地化键,disabled指示插件模块是否默认禁用,system指示该插件模块是否为系统插件模块(仅适用于非 OSGi)。你还可以有一个可选的description作为子元素。

  2. 创建一个可以在ModuleDescriptor类中使用的接口。这个接口将包含新模块所需的所有方法。例如,在字典中,我们需要一个方法来检索给定文本的定义,因此我们可以将接口定义如下:

    public interface Dictionary {
      String getDefinition(String text);
    }
    

    这种特定类型的新模块最终将实现此接口。

  3. 创建模块描述符类。该类必须继承AbstractModuleDescriptor类,并且应该使用我们创建的接口作为泛型类型。

    public class DictionaryModuleDescriptor extends AbstractModuleDescriptor<Dictionary> {
      ...
    }
    
  4. 实现getModule方法以创建模块。

    public class DictionaryModuleDescriptor extends AbstractModuleDescriptor<Dictionary> {
      public DictionaryModuleDescriptor(ModuleFactory moduleFactory) {
        super(moduleFactory);
      }
    
      public Dictionary getModule() {
        return moduleFactory.createModule(moduleClassName, this);
      }
    }
    

    在这里,我们使用ModuleFactory来创建这种类型的模块。

  5. 定义将在新模块类型中使用的属性和元素,并在init方法中检索它们。对于字典,我们至少需要一个属性,即language,以区分各种字典模块。让我们将该属性命名为lang并在init方法中检索它。现在,该类将类似于以下代码块:

    public class DictionaryModuleDescriptor extends AbstractModuleDescriptor<Dictionary> {
      private String language;
    
      public DictionaryModuleDescriptor(ModuleFactory moduleFactory) {
        super(moduleFactory);
      }
    
      @Override
      public void init(Plugin plugin, Element element) throws PluginParseException {
        super.init(plugin, element);
        language = element.attributeValue("lang");
      }
    
      public Dictionary getModule() {
        return moduleFactory.createModule(moduleClassName, this);
      }
    
      public String getLanguage() {
        return language;
      }
    }
    

    init方法以com.atlassian.plugin.Pluginorg.dom4j.Element作为参数,后者保存模块元素。在这里,我们已经检索到了'lang'属性,并将其赋值给一个局部变量,该变量有一个 getter 方法,可以在其他插件/模块中使用来获取语言值。

    我们可以根据新模块类型的要求添加更多属性或子元素。

  6. 有了这个,新的插件模块现在已经准备好了。我们现在可以编写新的字典类型的模块。

使用新模块类型创建模块

新的模块类型将简单到以下程度:

<dictionary key="myUSEnglishDictionary" lang="us-english" class="com.jtricks.dictionary.USDictionary" />
<dictionary key="myUKEnglishDictionary" lang="uk-english" class="com.jtricks.dictionary.UKDictionary" />

请注意,根元素与模块类型的键相同,在这种情况下是dictionary。每个都有其自己独特的key,并具有我们之前定义的lang属性。每个都有一个类,该类将适当地实现 Dictionary 接口。例如:

public class USDictionary implements Dictionary {
  public String getDefinition(String text) {
    if (text.equals("JIRA")){
      return "JIRA in San Fransisco!";
    } else {
     return "What are you asking? We in US don't know anything other than JIRA!!";
    }
  }
}

public class UKDictionary implements Dictionary {
  public String getDefinition(String text) {
    if (text.equals("JIRA")){
      return "JIRA in London!";
    } else {
      return "What are you asking? We in UK don't know anything other than JIRA!!";
    }
  }
}

使用创建的新模块

一旦定义了新模块,例如我们的myUSEnglishDictionarymyUKEnglishDictionary,我们可以在其他插件模块中使用它们。例如,如果我们想要在 servlet 模块中使用它们来查找 JIRA 的定义,可以按照以下步骤操作:

  1. 获取所有使用字典模块描述符的已启用模块。

    List<DictionaryModuleDescriptor> dictionaryModuleDescriptors = pluginAccessor.getEnabledModuleDescriptorsByClass(DictionaryModuleDescriptor.class);
    

    在这里,pluginAccessor可以按以下方式检索:

    PluginAccessor pluginAccessor = ComponentManager.getInstance().getPluginAccessor();
    

    它也可以用来检索所有使用给定模块描述符类的启用模块,如下所示。

  2. 对于每个DictionaryModuleDescriptorgetLanguage()方法将检索lang属性的值,而getModule()将检索相应的 Dictionary 实现类。例如,可以按如下方式检索uk-english的 JIRA 定义:

    private String getJIRADescription(String key) {
      // To get all the enabled modules of this module descriptor  List<DictionaryModuleDescriptor> dictionaryModuleDescriptors = pluginAccessor.getEnabledModuleDescriptorsByClass(DictionaryModuleDescriptor.class);
      for (DictionaryModuleDescriptor dictionaryModuleDescriptor : dictionaryModuleDescriptors){
        if (dictionaryModuleDescriptor.getLanguage().equals(key)){
          return dictionaryModuleDescriptor.getModule().getDefinition("JIRA");
        }
      }
      return "Not Found";
    }
    

    在这里,传递的关键是uk-english

它是如何工作的...

如果我们使用一个 servlet 来显示在所有部署的字典中查找 JIRA 单词的所有定义,我们的例子中包括 US 和 UK,结果将如下所示:

它是如何工作的...

在 JIRA 中启用访问日志

访问日志是查找 JIRA 实例中谁在做什么的好方法。在这个示例中,我们将看到如何在 JIRA 中打开访问日志记录。

如何做到这一点...

自 JIRA 4.1 以来,可以在管理 | 系统 | 用户 会话菜单中找到当前访问 JIRA 的用户列表。但是,如果您需要关于谁在做什么的更详细信息,则可以使用访问日志记录。

在 JIRA 4.x 中,可以通过转到管理 | 系统 | 日志记录 分析来启用访问日志记录,如下面的截图所示:

如何做到这一点...

我们可以分别打开HTTPSOAP访问日志,如下所示。还有一个额外的选项可以打开 HTTP dump 日志和 SOAP dump 日志。对于 HTTP,我们还可以在 HTTP 访问日志中包含图片。

所有这些日志默认情况下都是禁用的,如果通过 GUI 启用,将在下次重启时禁用。

为了永久启用它们,我们可以在位于 WEB-INF/classes 文件夹下的 log4j.properties 文件中,在下一节 Access logs 中打开它们,如下所示:

log4j.logger.com.atlassian.jira.soap.axis.JiraAxisSoapLog  = ON, soapaccesslog
log4j.additivity.com.atlassian.jira.soap.axis.JiraAxisSoapLog = false
log4j.logger.com.atlassian.jira.soap.axis.JiraAxisSoapLogDump  = ON, soapdumplog
log4j.additivity.com.atlassian.jira.soap.axis.JiraAxisSoapLogDump = false
log4j.logger.com.atlassian.jira.web.filters.accesslog.AccessLogFilter = ON, httpaccesslog
log4j.additivity.com.atlassian.jira.web.filters.accesslog.AccessLogFilter = false
log4j.logger.com.atlassian.jira.web.filters.accesslog.AccessLogFilterIncludeImages = ON, httpaccesslog
log4j.additivity.com.atlassian.jira.web.filters.accesslog.AccessLogFilterIncludeImages = false
log4j.logger.com.atlassian.jira.web.filters.accesslog.AccessLogFilterDump = ON, httpdumplog
log4j.additivity.com.atlassian.jira.web.filters.accesslog.AccessLogFilterDump = false

在 JIRA 4.x 之前启用访问日志

在 JIRA 4.x 之前,访问日志在 log4j.properties 文件中只有一个条目,我们可以通过将日志级别从 WARN 调整为 INFO 来启用它,如下所示:

log4j.category.com.atlassian.jira.web.filters.AccessLogFilter = INFO, console, filelog
log4j.additivity.com.atlassian.jira.web.filters = false

相同的选项也可以从 GUI 中从 WARN 级别调整到 INFO 级别,但与 4.x 中的情况一样,仅在下次重启前有效。

工作原理是怎样的...

一旦打开,SOAP 访问日志将被写入 atlassian-jira-soap-access.log 文件,SOAP dump 日志将被写入 atlassian-jira-soap-dump.log 文件,HTTP 访问日志将被写入 atlassian-jira-http-access.log 文件,而 HTTP dump 日志则被写入 atlassian-jira-http-dump.log 文件,所有这些文件都位于 logs 文件夹下。

你可以在访问日志中找到详细信息,类似于以下内容:

0:0:0:0:0:0:0:1 23x14x1 jobinkk [20/Jul/2011:00:23:43 +0100] "GET /secure/AdminSummary.jspa HTTP/1.1" 200 89148 466 "http://localhost:8080/secure/Dashboard.jspa" "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:5.0) Gecko/20100101 Firefox/5.0" "xdtgfh"
0:0:0:0:0:0:0:1 23x15x1 jobinkk [20/Jul/2011:00:23:50 +0100] "GET /secure/admin/ViewLogging.jspa HTTP/1.1" 200 7521 724 "http://localhost:8080/secure/AdminSummary.jspa" "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:5.0) Gecko/20100101 Firefox/5.0" "xdtgfh"
0:0:0:0:0:0:0:1 23x16x1 jobinkk [20/Jul/2011:00:23:55 +0100] "POST /secure/admin/WebSudoAuthenticate.jspa HTTP/1.1" 302 - 273 "http://localhost:8080/secure/admin/ViewLogging.jspa" "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:5.0) Gecko/20100101 Firefox/5.0" "xdtgfh"

还可以通过修改适当的属性在 log4j.properties 文件中更改单个日志文件的名称或路径。例如,通过修改 log4j.appender.soapaccesslog.File 属性,可以将 SOAP 访问日志文件写入 /var/log/soap-access.log

log4j.appender.soapaccesslog.File=/var/log/soap-access.log

在 JIRA 中启用 SQL 日志

与访问日志类似,另一个在调试问题时非常有用的日志片段是 SQL 日志。在这篇文章中,我们将看到如何打开 SQL 日志。

如何做...

SQL 日志不能从用户界面打开。相反,它可以在 WEB-INF/classes/log4j.properties 文件中打开,就像我们在访问日志中看到的那样。在这种情况下,需要修改的日志条目如下所示:

log4j.logger.com.atlassian.jira.ofbiz.LoggingSQLInterceptor = ON, sqllog
log4j.additivity.com.atlassian.jira.ofbiz.LoggingSQLInterceptor = false
log4j.logger.com.atlassian.jira.security.xsrf.XsrfVulnerabilityDetectionSQLInterceptor = ON, xsrflog
log4j.additivity.com.atlassian.jira.security.xsrf.XsrfVulnerabilityDetectionSQLInterceptor = false

后者记录了用于 Xsrf 漏洞检测的 SQL 查询。

工作原理是怎样的...

一旦打开,SQL 日志将被写入 atlassian-jira-sql.log 文件,位于 logs 文件夹下。

您可以找到执行的多个 SQL 的详细信息,如下所示:

2011-07-20 00:39:31,061 http-8080-6 jobinkk 39x31x1 1ogij3g /secure/EditIssue!default.jspa 0ms "SELECT ID, ENTITY_NAME, ENTITY_ID, PROPERTY_KEY, propertytype FROM PUBLIC.propertyentry WHERE ENTITY_NAME='IssueType' AND ENTITY_ID='3'"
2011-07-20 00:39:31,063 http-8080-6 jobinkk 39x31x1 1ogij3g /secure/EditIssue!default.jspa call stack ...

  at com.opensymphony.module.propertyset.ofbiz.OFBizPropertySet.getKeys(OFBizPropertySet.java:82)  at com.atlassian.jira.propertyset.PropertySetCache.bulkLoad(PropertySetCache.java:313)at com.atlassian.jira.propertyset.JiraCachingPropertySet.init(JiraCachingPropertySet.java:789)  at com.opensymphony.module.propertyset.PropertySetManager.getInstance(PropertySetManager.java:58)  at com.opensymphony.module.propertyset.PropertySetManager.getInstance(PropertySetManager.java:31)

与访问日志类似,可以通过修改 log4j.appender.sqllog.file 属性来更改 SQL 日志文件路径。

log4j.appender.sqllog.File=/var/log/sql.log

覆盖插件中 JIRA 的默认组件

JIRA 使用 PicoContainer 作为中央对象工厂。Picocontainer 负责实例化对象并解析它们的构造函数依赖关系。在 JIRA 中,许多 Manager、Service 和 Utility 类已经在 Picocontainer 中注册。注册发生在 ComponentRegistrar 类的 registerComponents() 方法中,可以通过 依赖注入 或使用 ComponentManager 类的 getter 方法或 getComponentInstanceOfType() 方法来检索这些类。

虽然大多数插件可以与这些已注册的组件以及使用组件插件模块创建的新组件一起工作,但有时需要覆盖 JIRA 中已注册的现有组件。在本节中,我们将学习如何实现这一点。

准备就绪

使用 Atlassian Plugin SDK 创建一个骨架插件。插件必须是 v1 版本。

如何做到...

在 JIRA 中覆盖现有组件也可以通过 组件 插件 模块来实现。但需要注意的是,插件必须是 v1 版本,并且应部署在 WEB-INF/lib 文件夹下才能生效。以下是简单但强大的步骤:

  1. 确定我们需要扩展的组件,并找出与之关联的接口。例如,假设我们要覆盖默认的 JIRA SubTaskManager。在这种情况下,接口将是 com.atlassian.jira.config.SubTaskManager

  2. atlassian-plugin.xml 中添加一个组件插件模块,接口参考 步骤 1

    <component key="subtaskManager" name="My Subtask Manager" class="com.jtricks.MySubtaskManager">        <interface>com.atlassian.jira.config.SubTaskManager</interface>
    </component>
    

    和往常一样,组件模块有一个唯一的 key 和一个可选的 name。在这里,类指向新组件的实现类。

  3. 创建组件插件模块中使用的实现类,在本例中是 com.jtricks.MySubtaskManager

    我们需要实现 SubTaskManager 接口中的所有方法,但如何实现完全由我们决定。如果我们只需要操作其中少数几个方法,这会更容易,因为在这种情况下,我们可以选择扩展 JIRA 的默认实现类,并只重写我们感兴趣的方法!

    为了简单起见,假设我们只需要修改 SubTaskManager 中的 createSubTaskIssueLink 操作,做一些额外的操作。为此,我们可以通过继承 JIRA 默认实现类 com.atlassian.jira.config.DefaultSubTaskManager 来创建 MySubtaskManager,并重写 createSubTaskIssueLink 方法:

    public class MySubtaskManager extends DefaultSubTaskManager {
      public MySubtaskManager(ConstantsManager constantsManager, IssueLinkTypeManager issueLinkTypeManager,  IssueLinkManager issueLinkManager, PermissionManager permissionManager, ApplicationProperties applicationProperties, CollectionReorderer collectionReorderer, IssueTypeSchemeManager issueTypeSchemeManager, IssueManager issueManager) {
        super(constantsManager, issueLinkTypeManager, issueLinkManager, permissionManager, applicationProperties,  collectionReorderer, issueTypeSchemeManager, issueManager);
       }
    
      @Override
      public void createSubTaskIssueLink(GenericValue parentIssue, GenericValue subTaskIssue, User remoteUser)   throws CreateException {              
    System.out.println("Creating Subtask link in overriden component using GenericValue!");
        super.createSubTaskIssueLink(parentIssue, subTaskIssue, remoteUser);
      }
    }
    

    在我们的示例中,假设我们只是将一行输出到日志,但在这里我们可以做更复杂的操作!

  4. 将插件打包并部署到 WEB-INF/lib 下。

它是如何工作的...

当 JIRA 启动时,所有与 PicoContainer 注册的默认组件会首先加载。但当插件加载时,如果存在使用相同接口且实现类不同的组件模块,那么该类将会为该接口注册。在我们的示例中,MySubtaskManager 将替代 DefaultSubTaskManager 类。

提示

如果是某些 Manager 类,这种方法可能会失败,可能是因为类加载的顺序问题。在这种情况下,您可能需要查看下一个部分!

如前所述,在重写 SubTaskManager 后,每当创建子任务时,我们将在服务器日志中看到打印的消息,如下图所示:

它是如何工作的...

还有更多...

在重写组件时,推荐使用 Component Plugins 模块,但也可以通过其他几种方式来实现相同的功能。

通过修改 JIRA 代码进行重写

对于修改了 JIRA 源分发版的人,重写组件可以通过一行代码完成。在创建了新的组件—实现我们希望重写的接口之后—我们可以修改 com.atlassian.jira.ContainerRegistrar 类中的 registerComponents 方法,将新类替代默认类。

例如,SubTaskManager可以通过替换以下内容进行重写:

register.implementation(PROVIDED, SubTaskManager.class, DefaultSubTaskManager.class);

使用:

register.implementation(PROVIDED, SubTaskManager.class, MySubtaskManager.class);

请注意,组件可以是 INTERNAL,意味着它们仅对 JIRA 本身可用,或 PROVIDED,在这种情况下,它们也会对 plugins2 插件可用。

通过扩展 PicoContainer 进行重写

在 JIRA 4.3 之前,JIRA 提供了一个扩展 PicoContainer 并在扩展后的 PicoContainer 中注册自定义组件的功能。以下是实现方式:

  1. 创建一个实现了 ContainerProvider 接口的新 PicoContainer 类。

    public class MyContainerProvider implements ContainerProvider{
      ...
    }
    
  2. 实现 getContainer 方法,从父容器构建一个新的容器,并包含新的注册。以我们之前的例子,该类将类似于以下代码块:

    public class MyContainerProvider implements ContainerProvider{
        private DefaultPicoContainer container;
    
        public PicoContainer getContainer(PicoContainer parent){
            if (container == null)
                buildContainer(parent);
            return container;
        }
    
        private void buildContainer(PicoContainer parent){
            this.container = new DefaultPicoContainer(new ProfilingComponentAdapterFactory(), parent);
      container.registerComponentImplementation(SubTaskManager.class, MySubtaskManager.class);
        }
    }
    

    在这里,MySubtaskManager 将以完全相同的方式创建。

  3. jira-application.properties 文件中注册新的容器提供者,该文件位于 atlassian-jira/WEB-INF/classes 文件夹中,使用的键为 jira.extension.container.provider

    jira.extension.container.provider = com.jtricks.MyContainerProvider
    
  4. 将包含新容器类和组件类的 JAR 文件部署到 WEB-INF/lib 文件夹下,并重启 JIRA。

在 JIRA 4.3 中此方法也有效,但已被弃用。从 4.4 开始,尽管仍然有效,但必须将 jira.extension.container.provider 属性添加到 jpm.xml 文件中,而不是 jira-application.properties 文件中。该属性将以如下方式添加:

<property>
    <key>jira.extension.container.provider</key>
    <default-value>com.jtricks.MyContainerProvider</default-value>
    <type>string</type>
    <user-editable>true</user-editable>
</property>

从电子邮件创建问题和评论

可以基于接收到的电子邮件消息自动在 JIRA 中创建问题或评论。此功能在帮助台等场景中非常有用,用户通常向指定的电子邮件地址发送电子邮件,支持团队则处理此类问题!

一旦正确配置,任何新接收到的电子邮件将会在 JIRA 中创建一个相应的问题,且对该问题的电子邮件通知回复将会作为评论创建在该问题上。也可以通过在 JIRA 启用的电子邮件附件功能,将文档附加到该问题上。如果没有启用外部用户管理,仍然可以创建用户账户—如果他们尚未拥有账户的话。

在本教程中,我们将展示如何配置 JIRA 以启用此功能。

如何操作...

以下是启用从电子邮件创建问题的步骤。

  1. 在服务器上创建一个电子邮件帐户——通常,每个 JIRA 项目一个电子邮件帐户。这个邮箱应该通过 POP、IMAP 或本地文件系统可访问。JIRA 将定期扫描此邮箱,并根据电子邮件创建问题或评论。

  2. 导航到 JIRA 的管理 | 全局 设置 | 邮件 服务器

  3. 点击配置新的 POP / IMAP 邮件服务器链接。

  4. 输入在步骤 1中创建的 POP 或 IMAP 邮件服务器的详细信息,然后点击添加

  5. 验证邮件 服务器页面上的详细信息,并在需要时进行修改。

  6. 导航到 JIRA 管理 | 系统 | 服务

  7. 添加一个新的服务,提供以下详细信息:

    1. 名称:服务的名称

    2. :从内置服务列表中选择一个。例如,com.atlassian.jira.service.services.pop.PopService

    3. 延迟:选择服务运行并扫描邮件的延迟时间。

  8. 添加服务将带您到编辑 服务页面。按照以下方式填写详细信息并更新:

    1. 处理程序:从下拉框中选择创建 评论 处理程序

    2. 处理程序 参数:这是最重要的部分,我们在此指定在创建问题时将使用的参数。以下是重要参数的列表:

      • project: 应该创建问题的项目的关键字。

      • issuetype: issuetype的唯一 ID。例如,如果我们希望创建一个 Bug 类型的问题,请将issuetype设置为 1。

      • createusers: 如果设置为 true,将为新发送者创建帐户。

      • reporterusername: 当发送者与现有用户不匹配时,可以用于创建带有指定报告人的问题。

      • notifyusers: 仅在createusers为真时使用。指示是否应向新创建的帐户的用户发送邮件通知。

      • ccassignee: 如果设置,则新问题将分配给“收件人”字段或“抄送”字段中匹配的用户(如果“收件人”字段没有匹配的用户)。

      • bulk: 确定如何处理“批量”电子邮件。可能的值有:

        • ignore: 忽略电子邮件并不执行任何操作。

        • forward: 将电子邮件转发到“转发电子邮件”文本框中设置的地址。

        • delete: 永久删除电子邮件。

      • catchemail: 如果添加,JIRA 将只处理发送到此地址的电子邮件。它在同一邮箱有多个别名时使用。

      • stripquotes: 如果启用,它将从回复中删除之前的消息。

        1. 转发电子邮件:错误通知和未处理的电子邮件(与批量转发处理参数一起使用)将被转发到此地址。

        2. 使用 SSL:如果使用 SSL,请选择 SSL。

        3. 服务器:选择此服务的电子邮件服务器。它将是我们在步骤 2步骤 4中添加的服务器。

        4. 端口:连接的端口。如果使用默认端口,请留空。

JIRA 现在已配置为接收发送到新添加的邮箱的邮件。

它是如何工作的...

我们在这里设置的服务每n分钟扫描一次邮箱(按照延迟配置),并获取新的来信。当接收到新邮件时,JIRA 会扫描主题,看是否提到了已存在的问题。如果有提到,电子邮件会作为评论添加到提到的问题上,电子邮件正文作为评论内容。如果主题中没有提到问题,JIRA 仍会检查电子邮件是否是对已创建问题的另一个电子邮件的回复。如果是,那么电子邮件正文会作为评论再次添加到该问题上。这是通过检查电子邮件中的 in-reply-to 标头来完成的。

如果 JIRA 仍然找不到任何匹配的问题,则会在项目中创建一个新的问题,并且类型由处理参数中的配置决定。电子邮件的主题将成为问题的摘要,电子邮件正文将成为描述。

电子邮件中的任何附件,无论是新的还是回复的,都将作为附件添加到问题中。

关于从电子邮件创建问题和评论的更多信息,请参见confluence.atlassian.com/display/JIRA/Creating+Issues+and+Comments+from+Email

同时,也值得检查插件交换平台,寻找具有扩展邮件处理功能的插件,这些插件能够在创建问题时添加更多的细节,例如自定义字段值。其中一些插件还拥有更好的过滤机制。

webwork 插件中的国际化

我们在前面的章节中已经看到如何编写 webwork 插件来创建新的或扩展的 JIRA 操作。在这个食谱中,我们将看到如何通过国际化本地化个性化这些插件中的消息。

正如 Wikipedia 所说:

“国际化和本地化是将计算机软件适应不同语言、地区差异和目标市场技术要求的手段。国际化是设计软件应用程序的过程,使其能够在不进行工程更改的情况下适应各种语言和地区。本地化是将国际化软件适应特定地区或语言的过程,方法是添加地区特定的组件并翻译文本。”

国际化和本地化的术语缩写为i18n,其中18代表国际化中从第一个i到最后一个n之间的字母数!

如何操作...

在 webwork 插件中实现国际化是通过一个与其关联的操作同名的资源包来实现的。以下是在 JIRA webwork 插件中启用国际化的步骤:

  1. 在插件的 src/main/resources 文件夹下,在与操作类同名的包结构中创建一个 properties 文件。

    例如,如果我们考虑前面示例中的 RedirectAction,属性文件将是 RedirectAction.properties,位于 src/main/resources/com/jtricks 文件夹下。

  2. 添加在 action 中需要使用的属性的键值对,如下所示:

    good.bye=Good Bye
    

    这里,good.bye 是将被使用的键,且会在所有语言的 properties 文件中保持一致。这里的值 "Good Bye" 将用于默认地区,但在其他语言的属性文件中将有相应的翻译。

  3. 在同一文件夹中为其他所需语言创建 properties 文件,格式为:${actionName}_${languageCode}_${countryCode}.properties。例如,如果我们需要为英国、美国和法国用户定制上述动作,属性文件名将如下:

    RedirectAction_en_US.properties
    RedirectAction_en_UK.properties
    RedirectAction_fr_FR.properties
    
  4. 在每个属性文件中添加 good.bye 属性,并为其设置适当的翻译值。例如,英文属性文件中的 Good Bye 属性值将会在法语文件中显示为 revoir

    good.bye=Good Bye (in RedirectAction_en_UK.properties)
    good.bye=revoir (in RedirectAction_fr_FR.properties)
    
  5. 在 action 类中,使用 getText(key) 方法来获取相应的消息。请记住,action class 扩展了实现了 getText 方法的 JiraWebActionSupport 类!

    例如,值 Good Bye 可以根据不同地区的用户语言显示如下:

    System.out.println(getText("good.bye"));
    

然而,这一魔法在 v2 插件中被打破,且 Atlassian 已经在 jira.atlassian.com/browse/JRA-23720 上报告了一个问题。解决方法是在 action 类中覆盖 getTexts 方法,如下所示:

@Override
public ResourceBundle getTexts(String bundleName) {
  return ResourceBundle.getBundle(bundleName, getLocale(), getClass().getClassLoader());
}

在这里,我们通过 action 类的类加载器获取 ResourceBundle,这解决了 v2 插件中的上述问题!

在结束之前,如果你需要在 velocity 模板中获取 i18N 文本,以下是步骤:

  1. 按照之前的方式添加属性文件。

  2. 如下所示,在 atlassian-plugin.xml 中添加资源条目:

    <resource name="common-18n" type="i18n" location="com.jtricks.RedirectAction"/>
    

    在这里,资源指向的是属性文件,并包含包名和文件名(省略国家或语言代码)。

  3. 使用 $i18n 对象来检索属性值,如下所示:

    $i18n.getText("good.bye")
    

在 v2 插件中共享公共库

本书已经介绍了如何创建 v1 和 v2 插件。v1 和 v2 插件之间的一个主要区别是,v1 插件可以访问应用程序类路径中所有可用的库和类,而 v2 插件无法访问它们。

例如,v1 插件可以通过将包含这些类的 JAR 文件放入 WEB-INF/lib 或将这些类添加到 WEB-INF/classes 下,来访问一些通用的工具类。但对于 v2 插件来说,这种方式行不通,因为它们需要将 JAR 文件嵌入到 MET-INF/lib 中,或者将类直接嵌入其中。那么,当我们需要在几个 v2 插件之间共享一个工具类时,该如何处理呢?我们是否应该将类嵌入到所有插件中?答案是否定的,在本教程中,我们将学习如何通过创建一个 OSGi 包来在 v2 插件之间共享这些工具类。

准备工作

使用 Atlassian 插件 SDK 创建一个骨架插件。

如何操作...

假设我们有一个数字工具类,用于整数的加法和乘法。如果我们想让这个类在所有 v2 插件中可用,应该怎么做?以下是步骤:

  1. 在正确的包下创建工具类:

    package com.jtricks.utilities;
    
    public class NumberUtility {
      public static int add(int x, int y) {
        return x + y;
      }
    }
    
  2. 导出需要共享的类,使其对其他 v2 插件可见。这一步非常重要。

    即使它是一个简单的工具类,我们仍然需要 atlassian-plugin.xml 来完成这一步。我们可以在 atlassian-plugin.xmlplugin-info 元素下使用 bundle-instructions 来将选定的包导出到其他插件/包中。

    bundle-instructions 元素允许子元素。

    导出 - :将选定的包从插件导出,以便在其他插件之间共享。

    导入 - :Tom 只将选定的包导入到插件中。默认情况下,它会导入来自其他插件的所有已导出的包。

    在这种情况下,我们需要修改 atlassian-plugin.xml 以导出我们的工具类,具体操作如下:

    <plugin-info>
      <description>Shared Utilities</description>
      <version>2.0</version>
      <vendor name="JTricks" url="http://www.j-tricks.com/" />
      <bundle-instructions>
        <Export-Package>com.jtricks.utilities</Export-Package>
      </bundle-instructions>
    </plugin-info>
    
  3. 可以选择只导出特定版本,并选择不导出某些包。更多细节可以参考 www.aqute.biz/Bnd/Bnd

  4. 可选地,我们可以使用 Import-Package 元素来导入上述已导出的包。默认情况下,它会被导入,因此这一步可以省略。不过,当你只想导入特定的包或使导入变为强制性时,这个步骤会很有用。更多详情可以参考上述链接。

  5. 打包插件并将其作为 v2 插件部署。

现在该工具类可以被所有其他 v2 插件访问。在开发时,其他插件应将此类包含在类路径中,这可以通过在 pom.xml 中将上述插件作为依赖项并设置作用域为 provided 来实现。

<dependency>
  <groupId>com.jtricks</groupId>
  <artifactId>utility-plugin</artifactId>
  <version>1.0</version>
  <scope>provided</scope>
</dependency>

当我们这样做时,上述方法 add 可以像类在同一插件中一样被调用。例如,RedirectAction 类可能有如下方法:

@Override
protected String doExecute() throws Exception {
  System.out.println("Action invoked. Doing something important before redirecting to Dashboard!");
  System.out.println("Sum:"+NumberUtility.add(3, 5));
  return getRedirect("/secure/Dashboard.jspa");
}

使用直接 HTML 链接的操作

可能有必要通过一个小提示结束本书,告诉大家如何通过简单地点击一个链接在 JIRA 中执行强大的操作,无论是从你的电子邮件、Web 表单还是 JIRA 本身!

几乎所有的操作都可以编码成单个 URL,只要我们拥有正确的参数来触发这些操作。别搞错了,这也有它的缺点,因为在某些情况下,它会覆盖所有的预处理、验证等操作。

执行操作的 URL 构造方式如下:

${baseUrl}/secure/${action}?${arguments}

其中,baseUrl是 JIRA 的基本 URL,action是要执行的 WebWork 操作,arguments是该操作所需的 URL 编码参数。参数由&分隔的键值对构成。每个键值对的格式为key=value,并且必须遵循 HTML 链接语法——即所有字符必须转义。让我们详细看看。

如何操作...

让我们从一个简单的例子开始,即创建问题。创建问题有四个阶段。

  • 进入初始创建屏幕

  • 选择项目和issuetype并点击下一步

  • 输入所有问题的详细信息

  • 点击提交并填写详细信息

我们可以在了解详细信息的前提下,逐步执行每个操作。以此为例,我们将http://localhost:8080/作为 JIRA 实例的基本 URL。

  1. 进入初始创建问题屏幕可以通过以下 URL 完成:

    http://localhost:8080/secure/CreateIssue!default.jspa

    请注意,最近的projectissuetype在访问此链接时会被预选中,因为这是 JIRA 的默认行为。但如果我们想要预选某个其他项目呢?我们只需在 URL 中添加pid参数,如下所示:

    http://localhost:8080/secure/CreateIssue!default.jspa?pid=10100

  2. 如果我们需要直接进入第二步,通过选择项目和问题类型,只需在 URL 中添加issuetype参数,并用&分隔。

    http://localhost:8080/secure/CreateIssue!default.jspa?pid=10100&issuetype=1

  3. 如果我们需要通过单击预填充创建问题对话框,请按如下所示在 URL 中输入所有详细信息,操作名称为CreateIssueDetails!init.jspa

    http://localhost:8080/secure/CreateIssueDetails!init.jspa?pid=10100&issuetype=1&priority=1&summary=Emergency+Bug&reporter=jobinkk

    请注意,所有必填字段都应填写,以避免验证错误。上述示例还展示了如何对 URL 进行编码,以符合 HTML 语法,将摘要中的空格替换为+。即,Emergency Bug 写作 Emergency+Bug,也可以写作 Emergency%20Bug。

  4. 如果我们希望通过单击立即创建问题并填写上述详细信息,可以使用CreateIssueDetails操作,而不是CreateIssueDetails!init

    http://localhost:8080/secure/CreateIssueDetails.jspa?pid=10100&issuetype=1&priority=1&summary=Emergency+Bug&reporter=jobinkk

    希望这能给我们一个关于如何通过直接链接执行操作的思路。在点击上述链接时,确保用户已登录或启用了匿名问题创建。

那么,如何找出涉及的操作类或需要传递的参数呢?

如果请求使用 GET 方法,你可以轻松地从浏览器 URL 中完成此操作。以选择了项目和 issuetype 创建问题(上述案例 2)为例,如下所示:

如何操作...

当请求为POST,如案例 4 所示,我们可以从 URL 中找出操作名称,但需要通过执行操作时提交的内容来确定参数。实现这一点有多种方式,其中一个简单的方法是使用浏览器功能。例如,使用Firebug与 Mozilla Firefox 可以获取执行操作时提交的参数,如下所示:

如何操作...

在这里,我们可以看到POST部分提交的参数:pid、issuetype、priority、summary 和 reporter。此外,我们还可以看到操作名称。一旦获得了参数列表,我们就可以在 URL 中使用它们,并用适当的值通过 & 符号分隔,正如我们在步骤 4 中看到的那样。

这种技术带来了很多可能性。例如,我们可以轻松地使用命令行工具如 wgetcurl 自动化提交这些我们构建的 URL。更多信息请参考:confluence.atlassian.com/display/JIRA/Creating+Issues+via+direct+HTML+links 和:confluence.atlassian.com/display/JIRACOM/Automating+JIRA+operations+via+wget

posted @ 2025-07-02 17:46  绝不原创的飞龙  阅读(52)  评论(0)    收藏  举报