Updating JIRA Plugins for JIRA 4.0
Plugin Developer Notes
JIRA 4.0 introduces several changes that may break existing plugins. If you are using a plugin that is not shipped with JIRA, the plugin may need to be updated to work with JIRA 4.0. If the plugin was written by you, please read through the information below and see if any of it is relevant to your plugin. If you are using a plugin written by a third party, please check with the plugin's author to see if the plugin has been tested with JIRA 4.0.
RPC plugin
A number of methods in the RPC plugin were refactored to use services provided by JIRA's core services layer. As a result they may now perform stricter validation on input data, in line with behaviour exhibited by JIRA's UI. A side effect of these changes is that method calls will now throw RemoteValidationException
instead of RemotePermissionException
for certain method calls. This change won't break client stubs, since all methods throw RemoteException
which is the superclass for RemoteValidationException
and RemotePermissionException
. However, if client code depends on RemotePermissionException
, it will need to be updated to expect a RemoteException
or RemoteValidationException
.
If you have developed custom code that uses JIRA's SOAP interface, the client code may need to be updated.
Responses from Servlet Plugin Modules are no longer decorated
The response generated by Servlet Plugin Modules served under /plugins/servlet
will no longer be decorated by SiteMesh. This means that if you are using servlets to display contents directly in the browser, they may be missing the JIRA header and footer. If the response from your servlet needs to be decorated, you have two possible solutions:
- The best is to convert the servlet to a Webwork Plugin Modules as this is better suited for processing requests that generate HTML responses.
- Alternatively, add
<meta content="decorator_name" name="decorator" />
<head>
element of your HTML response, wheredecorator_name
is the name of the SiteMesh decorator that should be applied.
Combined JavaScript servlet has been removed
In JIRA 4.0 we cleaned up a lot of the JavaScript resources that are included on every page. As a result, the combined-javascript
servlet was removed, in favour of Web Resources. This means that if your plugin defines javascript resources of the form:
<resource type="javascript">/path/to/my/resource.js</resource>
they will no longer be included. They should be replaced by Web Resources.
Project/Component/Version Tab Panel Plugins
The API for this plugin has changed. We removed the action being passed in (what were we thinking) and made it a cleaner, more consistent interface. If you have any custom Tab Panel Plugins plugins, you will need to update them to use the new interface:
/**
* Unified interface for all fragment-based tab panels.
*
* @since v4.0
*/
public interface TabPanel<D extends AbstractTabPanelModuleDescriptor, C extends BrowseContext>
{
/**
* Initialize the tab panel panel with the plugins ProjectTabPanelModuleDescriptor. This is usually used for
* rendering velocity views.
*
* @param descriptor the descriptor for this module as defined in the plugin xml descriptor.
*/
void init(D descriptor);
/**
* Used to render the tab.
*
* @param ctx The current context the tab is rendering in.
* @return Escaped string with the required html.
*/
String getHtml(C ctx);
/**
* Determine whether or not to show this.
*
* @param ctx The current context the tab is rendering in.
* @return True if the conditions are right to display tab, otherwise false.
*/
boolean showPanel(C ctx);
}
The specific plugin endpoints extend this in the following manner:
/**
* A Tab panel to be displayed on the Browse Project page.
*/
public interface ProjectTabPanel extends TabPanel<ProjectTabPanelModuleDescriptor, BrowseContext>
/**
* A Tab panel to be displayed on the Browse Component page.
*/
public interface ComponentTabPanel extends TabPanel<ComponentTabPanelModuleDescriptor, BrowseComponentContext>
/**
* A Tab panel to be displayed on the Browse Version page.
*/
public interface VersionTabPanel extends TabPanel<VersionTabPanelModuleDescriptor, BrowseVersionContext>
If you are using WebResourceManager.requireResource("...")
, your javascript will not be loaded when your tab is loaded via AJAX. You can include it via WebResourceManager.getStaticPluginResource()
in your actual content. Note: this will be fixed in the next beta.
Issue View Plugins
The com.atlassian.jira.plugin.issueview.IssueView
interface has changed such that the following methods:
public String getContent(Issue issue, IssueViewRequestParams issueViewRequestParams);
public void writeHeaders(Issue issue, RequestHeaders requestHeaders, IssueViewRequestParams issueViewRequestParams);
now take in the IssueViewRequestParams
parameter. This allows the plugin to access the parameters that were submitted with the request.
If you have written an Issue View plugin, you will need to update it such that in conforms to the new interface.
Issue Tab Panel Plugins
In JIRA 4.0, a new 'sortable' property was introduced to distinguish if the contents of an issue tab panel are sortable. If they are not, the sortable link in the top right corner will not be shown. By default issue tab panels are now not sortable. To make a tab panel sortable, plugin developers will have to add the following attribute:
<issue-tabpanel key="all-tabpanel" i18n-name-key="admin.issue.tabpanels.plugin.all.name" name="All Tab Panel" class="com.atlassian.jira.issue.tabpanels.AllTabPanel">
<description key="admin.issue.tabpanels.plugin.all.desc">Display all tab panels as one</description>
<label key="viewissue.tabs.all">All</label>
<order>0</order>
<sortable>true</sortable>
</issue-tabpanel>
Search Request View Plugins
In JIRA 4.0, the com.atlassian.jira.plugin.searchrequestview.SearchRequestView
has the following new method:
/**
* Prints the HTML headers for non-typical HTML such as Word or Excel views. (e.g.: requestHeaders.addHeader("content-disposition", "attachment;filename="sample.doc";");)
*
* @param searchRequest the original search request submitted by the user
* @param requestHeaders subset of HttpServletResponse responsible for setting headers only
* @param searchRequestParams context about the current search request
*/
public void writeHeaders(SearchRequest searchRequest, RequestHeaders requestHeaders, SearchRequestParams searchRequestParams);
If you have written a Search Request View Plugin, and the plugin implements the interface without extending com.atlassian.jira.plugin.searchrequestview.AbstractSearchRequestView
, you will need to update the plugin and implement the new method. The easiest thing to do is to proxy the call straight to the existing method:
/**
* Prints the HTML headers for non-typical HTML such as Word or Excel views. (e.g.: requestHeaders.addHeader("content-disposition", "attachment;filename="sample.doc";");)
*
* @deprecated since v3.13.3 please use {@link #writeHeaders(com.atlassian.jira.issue.search.SearchRequest, RequestHeaders, SearchRequestParams)}
* @param searchRequest the original search request submitted by the user
* @param requestHeaders subset of HttpServletResponse responsible for setting headers only
*/
public void writeHeaders(SearchRequest searchRequest, RequestHeaders requestHeaders);
Note that the SearchRequestParams
object used by Search Request View Plugins now extends IssueViewRequestParams
and therefore allows the plugin to access request parameters.
PortalManager and PortalPageConfiguration removed
The deprecated components PortalManager
and PortalPageConfiguration
have been removed. Developers should now be using the JiraDashboardStateStoreManager
to obtain similar functionality.
The PortalPageConfiguration
had methods that made changes directly to the database (e.g. store
, addPortletConfig
, deletePortletConfig
, deletePortletConfigs
, reload
). The PortalPage
does not have such methods. All persistent changes must now be made through the JiraDashboardStateStoreManager
passing the required DashboardState
as an argument.
The PortalPageManager & PortalPageService
may also be used to manipulate a PortalPage
within JIRA. These classes should no longer be used however since they will be re-written or removed for JIRA 4.1.
New Searching
The way a search is performed in JIRA has significantly changed. The introduction of advanced searching (JQL) necessitated a rewrite of the JIRA searching subsystem. In the process, the API for searching has also been changed (and improved) significantly. Unfortunately these changes will almost certainly mean that plugins that search will need to be updated for JIRA 4.0.
In JIRA 3.x and earlier, searching was achieved using a SearchRequest
in combination with SearchParameters
and SearchSorts
. While the SearchRequest
still continues to exist in JIRA 4.0, the SearchParameters
have been replaced with the Query
object.
/**
* The representation of a query.
*
*/
public interface Query
{
/**
* @return the main clause of the search which can be any number of nested clauses that will make up the full
* search query. Null indicates that no where clause is available and all issues should be returned.
*/
Clause getWhereClause();
/**
* @return the sorting portion of the search which can be any number of
* {@link com.atlassian.query.order.SearchSort}s that will make up the full order by clause. Null indicates that
* no order by clause has been entered and we will not sort the query, empty sorts will cause the default
* sorts to be used.
*/
OrderBy getOrderByClause();
/**
* @return the original query string that the user inputted into the system. If not provided, will return null.
*/
String getQueryString();
}
The Query
is JIRA's internal representation of a JQL search. It contains the search condition (i.e. the "where" clause) and the search order (i.e. the "order by" clause). The Query
object can be created using the JqlQueryBuilder
. For example, to create a query "find all issues assigned to either Dylan or Tokes that are unresolved and due in the next week" you would call:
final JqlQueryBuilder builder = JqlQueryBuilder.newBuilder();
builder.where().assignee().in("Dylan", "Tokes").and().unresolved().and().due().lt().string("+1w");
builder.orderBy().dueDate(SortOrder.ASC);
Query query = builder.buildQuery();
Once the Query
has been obtained, it can be used to execute a search. In JIRA 4.0 a new SearchService
has been added to provide a central location for Query
related operations. To run the search you can simply call SearchService.search()
as documented on the SearchService
. The SearchProvider
is still available for those who need to control the finer details of searching.
The Query
object is immutable; once it is created it cannot be changed. The JqlQueryBuilder
represents the mutable version of a Query
object. The JqlQueryBuilder
can be primed with an already existing Query
by calling JqlQueryBuilder.newBuilder(existingQuery)
.
In JIRA 3.x the SearchRequest
was the object that was passed to the searching system to perform a search. The Query
object has taken over this role in JIRA 4.0; the SearchProvider
and SearchService
now take in Query
objects rather than SearchRequests
. The SearchRequest
object has been reworked in JIRA 4.0 to significantly reduce its responsibility. For instance, ordering information is now stored on the Query
object rather than on the SearchRequest
object. The SearchRequest
really represents a saved search (aka. filter). You should only need to deal with SearchRequests
if you are working with filters. Even in this case, all searching operations need to be performed on Query
objects by calling SearchRequest.getQuery()
.
It is often necessary to get a URL for a particular Query
. The SearchService
provides the getQueryString(query)
method for this. The method returns a parameter snippet of the form jqlQuery=<jqlUrlEncodedQuery>
, which can be appended safely to an existing URL that points at the Issue Navigator. Note that the links that JIRA 4.0 generates are JQL based, so are incompatible with JIRA 3.x and before. Old valid JIRA 3.x URLs will still work with JIRA 4.0.
Given a Query
object it is possible to retrieve its JQL representation by calling either getGeneratedJqlString(query)
or getJqlString(query)
on the SearchService. The service makes sure that any values in the Query
that need to be escaped are handled correctly. Importantly, the Query.toString()
method does not return valid JQL (on purpose).
The SearchService.parseQuery(jqlString)
method can be used to turn a JQL string into its Query
representation. The return from this method has details on any parse errors encountered.
A Query
object, especially those parsed directly from the user, may not be valid. For example, the user may be trying to find issues in a status that does not exist. The SearchService.validateQuery(query)
method can be used to see if a particular Query
object is valid. Errors are returned with messages that can be displayed to the user. Executing an invalid Query
will not result in any errors and in fact may return results. To run an invalid query, JIRA will just make the invalid conditions equate to false and run the query. For example, searching for status = "I don't Exist" or user = bbain
will result in the query <false> or user = bbain
actually being run.
There are some methods on the SearchService
that we did not discuss here. Check out documentation on the SearchService
for more information.
例
Here's a complete example how to obtain search results for the query "project is JRA and the reporter is the currently logged in user and custom field with id 10490 contains 'xss'":
String jqlQuery = "project = JRA and reporter = currentUser() and cf[10490] = xss";
final SearchService.ParseResult parseResult =
searchService.parseQuery(authenticationContext.getUser(), jqlQuery);
if (parseResult.isValid())
{
try
{
final SearchResults results = searchService.search(authenticationContext.getUser(),
parseResult.getQuery(), PagerFilter.getUnlimitedFilter());
final List<Issue> issues = results.getIssues();
}
catch (SearchException e)
{
log.error("Error running search", e);
}
}
else
{
log.warn("Error parsing jqlQuery: " + parseResult.getErrors());
}
The preceding search could have also been written using the QueryBuilder:
final JqlQueryBuilder builder = JqlQueryBuilder.newBuilder();
builder.where().project("JRA").and().reporterIsCurrentUser().and().customField(10490L).eq("xss");
Query query = builder.buildQuery();
try
{
final SearchResults results = searchService.search(authenticationContext.getUser(),
query, PagerFilter.getUnlimitedFilter());
final List<Issue> issues = results.getIssues();
}
catch (SearchException e)
{
log.error("Error running search", e);
}
Plugging into JQL and what happened to my Custom Field Searchers
The introduction of advanced searching (JQL) necessitated a rewrite of the JIRA searching subsystem. Unfortunately these changes will certainly mean that any CustomFieldSearchers
will need to be updated to work in 4.0.
The most fundamental change is that all JIRA 4.0 searching is implemented using JQL. A JQL search consists of two components: firstly, a number of conditions, or Clauses
, that must be matched for an issue to be returned; and secondly, a collection of search orderings that define the order in which the issues should be returned. The Query
object is JIRA's internal representation of a search. It is now the responsibility of the CustomFieldSearcher
to take a relevant Query
, validate its correctness and generate a Lucene query to find issues that match it. By doing this your custom field becomes searchable using JQL.
The CustomFieldSearcher
and/or the custom field is also responsible for ordering results if the order in the search includes the custom field. If your custom field ordered correctly in JIRA 3.x, then it will order correctly in JIRA 4.0. While the internal representation of an order has changed in JIRA 4.0, it still uses the same interfaces to order the search results. We will not address ordering again.
What is a JQL Clause?
A custom field must process the Clauses
from a JQL search to integrate into JQL. Each Clause
consists of a number of conditions (e.g. abc != 20
) combined by the AND and OR logical operators (e.g. abc = 20 AND (jack < 20 OR jill > 34
). In JIRA a condition is represented by a TerminalClause
, the logical AND by an AndClause
and a logical OR by an OrClause
, all of which implement the Clause
interface. Finally, the logical NOT operator can be used to negate any other Clause
. It is represented by a NotClause
that also implements Clause. These Clause
objects are composed together to represent a complex conditions. For example, the condition abc = 20 AND NOT(jill > 34 OR NOT jack < 20)
is represented by the following tree:
A Clause
can be navigated by passing an instance of a ClauseVisitor
to the accept
method of a Clause. This follows the traditional visitor pattern.
The TerminalClause
represents a Clause
of the form "field operator value". Inside the TermincalClause
the "operator" is one of the values from Operator
enumeration while the "value" is represented as an Operand
. An Operand
can represent a single value (e.g. field = "single"
), a list of values (e.g. field in ("one", 1235)
), a function (e.g. field = function(arg1, arg2)
) or EMPTY (e.g. field is EMPTY
). In the end, all you want is the values from the Operand
. These can be obtained as a list of QueryLiteral
(see below) by calling JqlOperandResolver.getValues()
. The JqlOperandResolver
also has the isEmptyOperand
, isFunctionOperand
, isListOperand
and isValidOperand
methods that can be used to determine the type of the Operand
.
A QueryLiteral
represents either a String
, Long
or EMPTY
value. These three represent JQL's distinguishable types. It is up to the CustomFieldSearcher
to convert these values into something that makes sense to it. The type of a QueryLiteral
can be determined by calling its isEmpty
, getLongValue
or getStringValue
methods. The get methods will return null
or false
when the method and the QueryLiteral
type do not match.
Integrating with JQL
In JIRA 3.x a CustomFieldSearcher
was the way to provide customized searching functionality for custom fields. In JIRA 4.0 it is still the plugin point for searching; however, the CustomFieldSearcher
interface has changed significantly to accommodate the introduction of JQL. One of the major changes is that the CustomFieldSearcher
must return a CustomFieldSearcherClauseHandler
in JIRA 4.0. This object is a composition of a ClauseValidator
and a ClauseQueryFactory
.
The ClauseValidator
is used by JIRA to ensure that a JQL query is valid according to the CustomFieldSearcher
.
/**
* Validates a clause and adds human readable i18n'ed messages if there is a problem.
*
* @since v4.0
*/
public interface ClauseValidator
{
/**
* Validates a clause and adds human readable i18n'ed messages if there is a problem.
*
* @param searcher the user who is executing the search.
* @param terminalClause the clause to validate.
*
* @return an MessageSet that will contain any messages relating to failed validation. An empty message set must
* be returned to indicate there were no errors. null can never be returned.
*/
@NotNull
MessageSet validate(User searcher, @NotNull TerminalClause terminalClause);
}
It is up to the validator to ensure that the operator and the value from the passed TerminalClause
makes sense for the CustomFieldSearcher
and its associated custom field. Any errors can be placed in the returned MessageSet
. They should be internationalised with respect to the passed user.
The validate
method must always return a MessageSet
as its result. A null
return is not allowed. 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 will be displayed above the results.
The ClauseValidator
does not need to check if the passed TerminalClause
is meant for the for it, JIRA makes sure that it only passes TerminalClauses
that the ClauseValidator
is meant to process. It does that by only passing TerminalClauses
whose "field" matches one of the names the custom field must handle.
ClauseValidators
need to respect JIRA security. A ClauseValidator
should not leak information about JIRA objects that the searcher does not have permission to use. For example, a ClauseValidator
should not differentiate between an object not existing and an object that the user has no permission to see. A ClauseValidator
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.
The ClauseValidator
must be thread-safe and re-entrant to ensure correct behavior. JIRA will only create one instance of the ClauseValidator
per custom field instance. This means that multiple threads may be calling the validator at the same time.
The ClauseQueryFactory
is used by JIRA to generate the Lucene search for a JQL Clause.
public interface ClauseQueryFactory
{
/**
* Generates a lucene query for the passed {@link TerminalClause}....
*
* @param queryCreationContext the context of the query creation call; used to indicate that permissions should be
* ignored for "admin queries"
* @param terminalClause the clause for which this factory is generating a query.
* @return QueryFactoryResult contains the query that lucene can use to search and metadata about the query. Null
* cannot be returned.
*/
@NotNull
QueryFactoryResult getQuery(@NotNull QueryCreationContext queryCreationContext, @NotNull TerminalClause terminalClause);
}
It is the responsibility of the ClauseQueryFactory
to create the Lucene search for the passed TerminalClause
and QueryCreationContext
. The generated Lucene search is returned in the QueryFactoryResult
. The result contains the search (a Lucene Query object which is not related the the JQL Query object) and a flag to indicate whether or not the Lucene search should be negated. When set to true, JIRA will actually only match issues that do not match the returned Lucene search. For example, a ClauseQueryFactory
may decide to implement a condition like field != value
by returning a Lucene search that matches field = value
and setting the flag to true. You can also implement this condition by returning a Lucene search that matches field != value
and setting the flag to false.
The new argument here is the QueryCreationContext
. This object contains the variables that may be necessary when creating the query. The QueryCreationContext.getUser
method returns the user that is running 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. When it returns true
, the factory should assume that the searcher has permission to see everything in JIRA. When it returns false
, the factory should perform regular security checks.
A ClauseQueryFactory
should try to limit the queries so that issues that the user cannot see are excluded. Consider the query affectsVersion = "1.0"
. The ClauseQueryFactory
might detect that there are two versions named "1.0", one from project1
and the other from project2
. The factory might then notice that the user doing the search cannot see project1
. The factory can then return a query that contains only the version from project2
. This is mainly an efficiency concern as JIRA filters all search results to ensure users cannot see issues they are not allowed to.
The ClauseQueryFactory
does not need to check if the passed ClauseQueryFactory
is meant for it; JIRA makes sure that it only passes TerminalClauses
that the ClauseQueryFactory
is meant to process. It does that by only passing TerminalClauses
whose "field" matches one of the JQL names the custom field must handle. Put simply, the ClauseQueryFactory
must handle any passed TerminalClause
.
The ClauseQueryFactory
must also handle the situation when an invalid TerminalClause
is passed to it. An invalid TerminalClause
is one whose associated ClauseValidator
would not validate. The ClauseQueryFactory
must return an empty Lucene search if the passed TerminalClause
is invalid. Most importantly, the ClauseQueryFactory
must not throw an exception on an invalid TerminalClause
.
A ClauseQueryFactory
needs to be careful when implementing any of the negating operators (i.e. !=, !~, "not in"). These operators should not match what is considered empty by the custom field and CustomFieldSearcher
. For example, the JQL query resolution is EMPTY
will return all unresolved issues in JIRA. The query resolution != fixed
will only return all resolved issues that have not been resolved as "fixed", that is, it will not return any unresolved issues. The user has to enter the query resolution != fixed or resolution is EMPTY
to find all issues that are either unresolved or not resolved as "fixed".
A ClauseQueryFactory
also needs to consider field visibility. A CustomFieldSearcher
should not match any issues where its associated custom field is not visible. Importantly, asking for EMPTY
should not match issues where the custom field is not visible. For example, the JQL query resolution is EMPTY
will not return issues from a project whose resolution field has been hidden. A hidden field is assumed not to exist.
There are some extra interfaces that the CustomFieldSearcherClauseHandler
may also implement to provide optional functionality to the searching subsystem:
- ValueGeneratingClauseHandler: Gives the
CustomFieldSearcher
the ability to suggest some values during JQL entry auto-complete. This is really only useful for custom fields whose values come from an allowable finite set. - CustomFieldClauseSanitiserHandler: Gives the
CustomFieldSearcher
the ability pre-process the query and remove sensitive information from the query before it is displayed to the passed user. - CustomFieldClauseContextHandler: Gives the
CustomFieldSearcher
the ability to customize JIRA's query context calculation. This interface is best left alone, unexplained and unimplemented.
Integrating into the Issue Navigator
The good old Issue Navigator still exists. The Issue Navigator actually has two modes: simple and advanced. The simple mode is what was considered the Issue Navigator in JIRA 3.x. Each searcher on the simple Issue Navigator represents a Clause
. For example, selecting "JIRA" in the project searcher produces the Clause
project = JIRA
. Using multiple searchers is achieved by ANDing the multiple implied Clauses
together. In this way the simple Issue Navigator actually generates JQL.
The advanced mode shows the raw JQL to the user. It allows a user to search by entering arbitrary JQL. Since it simply shows JQL, it is possible to create a query using the simple Issue Navigator and then view it in the advanced Issue Navigator. However, it may not always be possible to go from the advanced Issue Navigator to the simple Issue Navigator, as the simple view only allows a very limited set of JQL. A JIRA user will be able to move from the advanced to the simple Issue Navigator when the current JQL can be represented in the simple view. JIRA will stop a user from transitioning from the advanced to the simple Issue Navigator when the JQL is just too complicated to represent correctly.
The CustomFieldSearcher
itself is still responsible for integrating into the Issue Navigator. The CustomFieldSearcher
extends from the IssueSearcher
, which has undergone major cosmetic surgery in JIRA 4.0. The main change is that the methods on the IssueSearcher
have been relocated to new interfaces that the IssueSearcher
composes. For example, JIRA 3.x used to call issueSeacher.getEditHtml()
to get the searcher's HTML but now in 4.0 it calls issueSeacher.getSearchRenderer().getEditHtml()
. The following table shows a summary of all the changes:
Old Searcher Method |
New Seacher Interface |
New Seacher Method |
コメント |
---|---|---|---|
|
Inserted a new |
||
|
|
|
Inserted a new |
|
|
|
Inserted a new |
|
|
|
Inserted a new |
|
|
|
|
|
|
|
|
|
|
|
Added in JIRA 4.0. See description below. |
|
|
|
|
|
|
|
Added in JIRA 4.0. See description below. |
|
|
Inserted a new |
|
|
|
|
Inserted a new |
|
|
|
Changed the method name and arguments to work with JQL. See the discussion below. |
|
|
|
Added in JIRA 4.0. See discussion below. |
|
|
|
Changed the method name and arguments to work with JQL. See the discussion below. |
|
|
|
Removed as it is no longer necessary. |
|
|
|
Removed as it is no longer necessary. |
|
|
|
Removed as it is no longer necessary. |
|
|
|
Removed as it is no longer necessary. |
The SearcherRender
interface groups together the rendering related IssueSearcher
actions. The new method isRelevantForQuery
takes over the role from the isRelevantForSearchRequest
method. Its job is to take a complete Query
object and determine if the CustomFieldSearcher
is relevant for that Query
. The result is used to decide if the HTML from the getViewHtml
is included on some JIRA pages. As a general rule, this essentially involves walking the Query
and looking for TerminalClauses
related to the CustomFieldSearcher
. For example:
final NamedTerminalClauseCollectingVisitor clauseVisitor = new NamedTerminalClauseCollectingVisitor(clauseNames.getJqlFieldNames());
if (query != null && query.getWhereClause() != null)
{
query.getWhereClause().accept(clauseVisitor);
}
return clauseVisitor.containsNamedClause();
This code essentially walks the tree looking for all TerminalClauses
that have a particular set of names. The Query
is relevant if such a Clause
exists or is not relevant otherwise.
The isRelevantForQuery
method is only called if the passed Query fits in the simple Issue Navigator.
The SearcherInformation
interface groups together methods that return data about the IssueSeacher
into a single interface. The SearcherInformation.getField
method simply returns the Field
associated with the searcher. This information is available to the searcher once the CustomFieldSearcher.init()
is called by JIRA.
SearcherInformation.getSearcherGroupType
is a method that returns the group the searcher should be seen in on the navigator. The custom field has to return SearcherGroupType.CUSTOM
. JIRA will always force this value even if it is specified as something different.
The SearchInputTransformer
interface groups together those methods on the IssueSearcher
that convert Query
objects into different forms so that they can be displayed and manipulated using the simple Issue Navigator. The simple Issue Navigator does not have the ability to represent all possible JQL queries. The SearchInputTransformer.doRelevantClausesFitFilterForm
method allows JIRA to ask the CustomFieldSearcher
if the passed Query
can be represented in the simple Issue Navigator. This is used by JIRA to stop people trying to view complex JQL in the simple Issue Navigator. When this call is made, the CustomFieldSearcher
must decide if the relevant sections of the passed Query
can be represented in the simple Issue Navigator form. Irrelevant Clauses
(i.e. those Clauses
unrelated to the Searcher) should be ignored. The method must return true when the Query
is not at all relevant. This method is normally implemented by walking the Query
and checking that any relevant TerminalClauses
are connected via the correct set of logical conditions. For example, here is some common code encountered with JIRA's internal searchers:
if (query != null && query.getWhereClause() != null)
{
final Clause whereClause = query.getWhereClause();
final SimpleNavigatorCollectorVisitor collector = new SimpleNavigatorCollectorVisitor(clauseNames.getJqlFieldNames());
whereClause.accept(collector);
if (!collector.isValid() || collector.getClauses().size() > 1)
{
return false;
}
else if (collector.getClauses().size() == 1)
{
final TerminalClause terminalClause = collector.getClauses().get(0);
return checkOperator(terminalClause.getOperator()) && checkOperand(terminalClause.getOperand(), true);
}
}
return true;
The code starts by creating a ClauseVisitor
that will find all the TerminalClauses
with particular names. This visitor will also detect whether or not the all paths from the root Clause
of the tree to the TerminalClauses
are only through AndClauses
. This check is made to ensure that these TerminalClauses
form part of a simple AND expression since the simple Issue Navigator can only support AND operators between Clauses. The code also ensures that only one TerminalClause
is found since this is what the CustomFieldSearcher
generates for the simple Issue Navigator. Note that the method will return true if no relevant TerminalClauses
are found.
The new SearchInputTransformer.populateFromQuery
method replaces the old populateFromSearchRequest
. It essentially takes the passed Query
and serialises the relevant parts into their associated FieldValuesHolder
representation. It is up to this CustomFieldSearcher
to work out which parts of the Query
are relevant to it. It must ignore those parts of the Query
that it was not designed to handle. This method will only be called if it is known that the Query
fits in into the simple Issue Navigator. It is generally implemented by walking the tree and looking for the relevant TerminalClauses
and subsequently serialising them into the passed FieldValuesHoldler
. For example:
if (query.getWhereClause() != null)
{
final ClauseVisitor visitor = new DateSerializer();
query.getWhereClause().accept(visitor);
fieldValuesHolder.put(dateSeacherConfig.getPreviousField(), visitor.getPreviousDate());
fieldValuesHolder.put(dateSeacherConfig.getNextField(), visitor.getPreviousDate());
}
In this example we used a ClauseVisitor
that walks the Query
and calculates the parameters for a date-based searcher. Once the visitor is run, we simply add the calculated parameters to the FieldValuesHolder
.
The SearchInputTransformer.getSearchClause
method replaces the old populateSearchRequest
. Its job it to take a take the relevant values from the FieldValuesHolder
and generate a Clause
for them. This Clause
will be combined with the Clauses
from other active searchers using the AND operator to produce the final Query on the simple Issue Navigator. The irrelevant values from the FieldValueHolder
must be ignored. This method is generally called after JIRA has called SearchInputTransformer.populateFromParams
with the web parameters returned from the filter form, that is, this method is how the filter form is converted into a Clause
and subsequently a Query. Consider the following example:
final Clause relativeClause = createPeriodClause((String) fieldValuesHolder.get(dateSearcherConfig.getPreviousField()),
(String) fieldValuesHolder.get(dateSearcherConfig.getNextField()));
final Clause absoluteClause = createDateClause((String) fieldValuesHolder.get(dateSearcherConfig.getAfterField()),
(String) fieldValuesHolder.get(dateSearcherConfig.getBeforeField()));
return createCompoundClause(relativeClause, absoluteClause);
This example demonstrates how a date field looks in the FieldValueHolder
for its relevant properties and uses them to create a Clause
. This example also shows that the returned Clause
can be as complex as the CustomFieldSearcher
wants.
The SearchInputTransformer.getSearchClause
and SearchInputTransformer.populateFromQuery
really form a pair. The Clause
returned from SearchInputTransformer.getSearchClause
must be correctly processed by SearchInputTransformer.populateFromQuery
. If this does not occur, then it would be possible to generate a query in the simple Issue Navigator view that cannot actually be viewed in it. This also implies passing the Clause
object returned from SearchInputTransformer.getSearchClause
to the SearchInputTransformer.doRelevantClausesFitFilterForm
must return true.
JIRA 3.x to 4.0 Filter Upgrade
In JIRA 3.x saved seaches (aka. filters) were stored in the database as XML. In JIRA 4.0, all searchers are stored directly as JQL. An upgrade task has been written to convert 3.x filters into JQL. Unfortunately, there is no way for plugin developers to integrate into this upgrade task. This essentially means that the upgrade may fail if you have a custom SearchParameter
or use an existing SearchParameter
in an unorthodox way. JIRA will inform users through e-mail if any of their filters could not be upgraded cleanly. The administrator is also made aware of any problems through JIRA's log files.
Converting Portlets to Gadgets
JIRA 4.0 introduces a new dashboard based on the OpenSocial specification. Legacy portlets will still be supported, but they will miss out on a lot of new features (e.g. displaying the gadget on iGoogle). As such you may wish to convert your plugin's portlets to gadgets. To do so please follow the documentation available in the Gadget Development Hub, as well as the instructions for writing a plugin upgrade task to convert any portlet settings that users may have saved.