Updating JIRA Plugins for JIRA 4.0

Still need help?

The Atlassian Community is here for you.

Ask the community

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:

  1. The best is to convert the servlet to a Webwork Plugin Modules as this is better suited for processing requests that generate HTML responses.
  2. Alternatively, add
    <meta content="decorator_name" name="decorator" />
    in the <head> element of your HTML response, where decorator_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.

Examples

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

Comments

getEditHtml

SearchRenderer

getEditHtml

Inserted a new User parameter as the first argument.

getViewHtml

SearchRenderer

getViewHtml

Inserted a new User parameter as the first argument.

isShown

SearchRenderer

isShown

Inserted a new User parameter as the first argument.

isRelevantForSearchRequest

SearchRenderer

isRelevantForQuery

Inserted a new User parameter as the first argument. See description below.

getId

SearcherInformation

getId

 

getNameKey

SearcherInformation

getNameKey

 

 

SearcherInformation

getField

Added in JIRA 4.0. See description below.

getRelatedIndexers

SearcherInformation

getRelatedIndexers

 

 

SearcherInformation

getSearcherGroupType

Added in JIRA 4.0. See description below.

populateFromParams

SearchInputTransformer

populateFromParams

Inserted a new User parameter as the first argument.

validateParams

SearchInputTransformer

validateParams

Inserted a new User parameter as the first argument.

populateFromSearchRequest

SearchInputTransformer

populateFromQuery

Changed the method name and arguments to work with JQL. See the discussion below.

 

SearchInputTransformer

doRelevantClausesFitFilterForm

Added in JIRA 4.0. See discussion below.

populateSearchRequest

SearchInputTransformer

getSearchClause

Changed the method name and arguments to work with JQL. See the discussion below.

register

 

 

Removed as it is no longer necessary.

getQuerySnippet

 

 

Removed as it is no longer necessary.

getStringValue

 

 

Removed as it is no longer necessary.

getName

 

 

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.

Last modified on Apr 27, 2010

Was this helpful?

Yes
No
Provide feedback about this article
Powered by Confluence and Scroll Viewport.