Available: | Jira 4.0 and later. |
The introduction of advanced searching (that is, JQL) significantly enhances Jira searching functionality.
JQL functions are among the extension points that JQL provides to developers. Functions provide a way for values within a JQL query to be calculated at runtime. They are simple to write and can be surprisingly powerful.
For example, consider the issueKey
clause in JQL. It matches an issue with a particular issue key.
This in itself is not very useful, but when combined with a function that returns all of a user's watched
issues (watchedIssues
), it provides a way to find all the issues that the current user is watching
(issuekey
in watchedIssues()
).
JQL functions can only provide values to a query; most importantly, they cannot be used to process the results.
For example, it is not possible to write a JQL function that will calculate the total time remaining from all issues returned from a search. Consequently, functions can only be used with JQL clauses that already exist. The only way to implement new JQL clauses is to implement a new searchable custom field. While this gives more control to the app developer, it is much more complicated.
JQL functions can take arguments. These arguments must take the form of simple string values.
For example, fixVersion
in releasedVersions('JIRA')
contains a function call to releasedVersions
to find
all the released versions in the JIRA
project. Making the arguments simple strings means that JQL lists and
other JQL functions cannot be used as arguments. For example, it is not possible to do something like myFunction(currentUser())
.
A JQL function is an implementation of the
JqlFunction
interface that is registered in Jira as a jql-function
app. The registered JqlFunction
will only be instantiated
once per jql-function
app. All queries that use the function will share the single instance. Consequently,
a function can be called by multiple threads at the same time and as such must be thread-safe.
Here is an example of jql-function
defined in the
atlassian-plugin.xml file:
1 2<jql-function key="example-function" i18n-name-key="example.plugin.name" name="Example Plugin Function" class="com.atlassian.example.jira.ExampleFunction"> <description key="example.plugin.description">JQL function to make something cool</description> <fname>exampleFunc</fname> <list>true</list> </jql-function>
Name | Description |
---|---|
key | The unique identifier of the plugin module. You refer to this key to use the resource from other contexts in your app, such as from the plugin Java code or JavaScript resources. <jql-function key="myJqlFunc"/>
Required: yes. |
i18n-name-key | The localization key for the human-readable name of the plugin module. Required: no. |
name | The app name for the human-readable name of the plugin module. Will be used if Required: no. |
class | The Java class of the JQL function module. The custom JQL function class must implement the com.atlassian.jira.plugin.jql.function.JqlFunction interface, or extend a class that does. Required: yes. |
Name | Description |
---|---|
fname | This element specifies the name of the function. |
list | This element specifies whether this function returns a list of values or a single value. If omitted, the default is false. |
description | The description of the plugin module. The That is, the description of the JQL function. |
In the following sections we go through the JqlFunction
methods.
The JqlFunction.init()
method is called by Jira to tell the JqlFunction
about its associated
JqlFunctionModuleDescriptor.
This object represents Jira's view of the JqlFunction
and can be used to find app resources.
The init
method is only called once and is guaranteed to be called before the function is actually used by Jira.
The JqlFunction.getFunctionName method returns the name that can be used in JQL to invoke the function.
1 2public String getFunctionName() { return "exampleFunction"; }
You can extend AbstractJqlFunction
, so the fname
element's value will be returned. Jira must get
the same name each time it calls getFunctionName
. Importantly, this means that the function name
cannot be translated. The function name does not have to be in English, however, it must be in the
same language for every user in Jira irrespective of their language settings.
The function name should also be unique across all instances of Jira where it is expected to run. Having two JQL functions of the same name in Jira will produce confusing results. Jira will only register the first function for use in Jira and will simply ignore any others of the same name. The app that Jira determines to be first is somewhat arbitrary and may result in different JQL functions of the same name being registered on each start.
The moral of the story: try very hard to make your function names unique.
The JqlFunction.getMinimumNumberOfExpectedArguments
returns the smallest number of arguments that the function
can accept. The value returned from this method must be consistent across method invocations.
1 2public int getMinimumNumberOfExpectedArguments() { return 1; }
The JqlFunction.isList should return:
true
if the function returns a list.false
if it returns a scalar.The main difference is that a list type can be used with the IN
and
NOT IN
operators while a scalar type can be used with =
, !=
, <
, >
, <=
, >=
, IS
, and IS NOT
.
1 2public boolean isList() { return true; }
You can extend AbstractJqlFunction
, so the list
element's value will be returned.
The value returned from this method must be constant. It cannot change based on the parameters or the function's result.
The function must either always return a list or must always return a scalar.
The easiest way to work out whether the function should return a list or not is to simply consider
where it is going to be used. If the function makes sense with the IN
or NOT IN
operators, it
returns a list and needs to return true
for this method. This will normally be the case when the
function logically returns more than one value (for example, releasedVersons()
, membersOf()
). On the
other hand, if the function should be used with =
, !=
, <
, >
, <=
, >=
, IS
, and IS NOT
,
it should return false
. This will normally be the case when a function logically returns one value
(for example, now()
, currentUser()
).
The JqlFunction.getDataType
method is called to determine the type of data the function returns.
The value tells Jira which JQL clauses the function can be expected to work with. For example, returning
JiraDataTypes.VERSION
indicates that the function should only be used with clauses that work with Jira versions.
You can return JiraDataTypes.ALL
if you wish the function to be available across all JQL conditions.
1 2public JiraDataType getDataType() { return JiraDataTypes.ALL; }
Again, the value returned must be consistent across all invocations of this method.
The JqlFunction.validate
method is called by Jira when the function needs to be validated.
The job of this method is to check the arguments to the function to ensure that it is used correctly.
Here is the interface:
1 2@NotNull MessageSet validate(ApplicationUser searcher, @NotNull FunctionOperand operand, @NotNull TerminalClause terminalClause);
The most important argument is the
FunctionOperand.
It contains all of the functions arguments as given by the FunctionOperand.getArgs
method.
All JQL function arguments come in as Strings
and it is up to the function to interpret them correctly.
The searcher
is the user for whom the function should be
validated, that is, the user for whom any security checks should be performed.
The TerminalClause
is Jira's representation of the JQL condition we validate.
For functions it represents a JQL condition of the form name operator function(arg1, arg2, ..., argn)
.
The name, operator, and function can be returned by calling TerminalClause.getName
, TerminalClause.getOperator
,
and TerminalClause.getOperand
respectively.
The value returned from getOperand()
will be the FunctionOperand
that is passed to this method.
This method is only called when the passed arguments are relevant to the JQL function, that is, the validation
does not need to check if the FunctionOperand
has the correct function name.
The validate
method must always return a
MessageSet
as its result; a null
return is prohibited. A MessageSet
is an object that contains all of the errors
and warnings that occur during validation.
All messages
in the MessageSet
need to be translated with respect to the passed searching user. An empty MessageSet
indicates that no errors have occurred. A MessageSet
with errors indicates that the JQL is invalid
and should not be allowed to run. The returned messages will be displayed to the user so that any problems
may be rectified. A MessageSet
with warnings indicates that the JQL may have problems but that it
can still be run. Any warning messages are displayed above the results.
Functions need to respect Jira security. A function should not return references to Jira objects (for example, projects, issues) that the user is not allowed to see. Further, a function should not leak information about Jira objects that the searcher does not have permission to use. For example, a function should not differentiate between a project not existing and a project that the user has no permission to see. A function that behaves badly will not cause JQL to expose issues that the searcher is not allowed to see (since JQL does permission checks when it runs the filter), though it does open up an attack vector for information disclosure.
Only one instance of each JQL function is created. This means that your function can (and probably will) be called by two threads at the same time. To accommodate this, your function must be thread-safe or unexpected behavior can result.
1 2public MessageSet validate(final ApplicationUser searcher, final FunctionOperand operand, final TerminalClause terminalClause) { return new MessageSetImpl(); }
The implementation of this method must be thread-safe. The dependencies should be thread-safe and
stored in final
or volatile
variables to ensure visibility. All method state is kept local
to ensure that it is not visible to other threads.
The JqlFunction.getValues
method is called by Jira when it needs to execute the function so that it can
perform a query.
1 2@NotNull List<QueryLiteral> getValues(@NotNull QueryCreationContext queryCreationContext, @NotNull FunctionOperand operand, @NotNull TerminalClause terminalClause);
The FunctionOperand
and the TerminalClause
are as described previously in the JqlFunction.validate
method. The new argument here is the
QueryCreationContext.
This object contains the variables that may be necessary when executing the function.
The QueryCreationContext.getUser
method returns the user that runs the search and as such should be used to perform any security
checks that may be necessary.
The QueryCreationContext.isSecurityOverriden
method indicates whether
or not this function should actually perform security checks.
true
, the function should assume that the searcher has permission to see everything in Jira.false
, the function should perform regular Jira security checks and make sure it only
returns things that the searcher has permission to see. This parameter is used by Jira in certain administrative
operations where finding all issues is important.The JQL function returns a list of
QuerylLiteral.
A QueryLiteral
represents either a String
, Long
, or EMPTY
value. These three represent JQL's distinguishable
types. The type of the QueryLiteral
is determined at construction time and cannot be changed:
EMPTY
.String
and it represents a String
.Long
and it represents a Long
.Most JQL clauses will treat each type differently. For example, let's consider the affectsVersion
clause. When passed a Long
QueryLiteral
, it will look for all issues with an Affects Version of the
specified ID. This is useful when a function would need to identify a particular version exactly.
Where possible, we suggest that functions try to return IDs so that query results are unambiguous.
When passed a String
QueryLiteral
, the affectsVersion
clause will run one of two searches depending
upon the value in the QueryLiteral
:
QueryLiteral
exist, then return all issues with the specified
Affects Version(s). This may return empty results.QueryLiteral
can be parsed into a version ID and that version exists, then return
all issues that have an Affects Version of the parsed ID. This may return empty results.JQL functions may return String
QueryLiterals
. However, the result of the query will depend on the lookup
procedure of the JQL clause it is used with. Finally, the EMPTY
QueryLiteral
will make the affectsVersion
condition look for all issues that have no Affects Version set.
The function always returns a list of QueryLiteral
objects. It is even valid for a scalar function (that is,
a function whose JqlFunction.isList
method returns false
) to return multiple QueryLiteral
objects.
In such a situation it is the JQL clause the function is being used with that decides what this means. All of the
core Jira JQL clauses simply treat such a situation as an OR
between each of the returned values. The function
must return an empty list of QueryLiteral
objects (not an empty QueryLiteral
) to indicate an error. Importantly,
the function can never return a null
list.
The JqlFunction.getValues
method may be called with arguments that would not pass the JqlFunction.validate
method. Under this situation it is important that the function does not throw an error, as JQL is designed to try
and run invalid queries where possible. The function should run, if possible, or otherwise return an empty list.
The only thing the function can assume is that the FunctionOperand
argument is meant to be executed by the function.
Only one instance of each JQL function is created. This means that your function can (and probably will) be called by two threads at the same time. To accommodate this, your function must be thread-safe or unexpected behavior can result.
The JqlFunction.getValues
method must execute quickly. Keep in mind, that your function will be executed each
time the query is run. If your function takes 10 seconds to run, then the entire JQL query will take at least
10 seconds. Functions also need to perform well under concurrent load. Keep synchronization (locking) down to a
minimum. The simplest way to do this is to keep all the functions' calculation state on the stack and out of member variables.
1 2public List<QueryLiteral> getValues(final QueryCreationContext queryCreationContext, final FunctionOperand operand, final TerminalClause terminalClause) { return Collections.emptyList(); }
Function sanitization is important to make function production ready. A saved JQL search (filter) can be shared
with multiple users. While this functionality is very useful, it also allows information to be leaked.
For example, let's say you have a filter that contains assignee in exampleFunc(Administrators, Proj)
and you
share the filter with Janice who cannot see Proj
. The search will not return any results, however, Janice will
know that a project called Proj
exists even though she does not have permission to see it.
A JQL function that can expose sensitive information (that is, a function that does security checks) should also implement the optional ClauseSanitisingJqlFunction interface.
The interface has one method:
1 2@NotNull FunctionOperand sanitiseOperand(User searcher, @NotNull FunctionOperand operand);
This method takes a searcher and a FunctionOperand and returns an equivalent operand that hides any privileged information the passed searcher should not see. The returned function is what the passed searcher will see when trying to load the filter.
It is important that the FunctionOperand
that is returned from sanitization is equivalent to the passed operand.
If this is not the case, then it is possible for two people running the exact same filter to be actually
running two different searches.
1 2public FunctionOperand sanitiseOperand(final User user, final FunctionOperand functionOperand) { return functionOperand; }
Rate this page: