Skip to main content

JIRA, Groovy and the REST of it

Posted by rah003 on February 20, 2013 at 12:17 AM PST

Atlassian has made some seriously great products, for example the project tracker JIRA. We recently upgraded our rather archaic version of JIRA at Magnolia to the latest and greatest offering. Everyone in the team found something in this upgrade. Some of us were desperate to use GreenHopper, others were looking forward to a better LDAP integration, and my personal favourite was the new REST API.

One thing I do a lot is produce reports about various aspects of issues and progress made when solving them. Therefore, I went on the hunt for a plugin that would do everything I need and export that in a nicely formatted report on top of that - but no luck. You'll understand I was really excited when we finally went through with the upgrade and I could start using the API.

However, I ran into some problems: I found plenty of documentation about the API and responses returned by JIRA. I even found a bunch of examples of how to use the API from Bash or Python - but no example specifically written in Groovy. I didn't consider that a big deal, though: it's just another REST API and I didn't think that there would be a problem using it. I was a bit surprised that the reality was not as simple as I had expected. So for anyone else who needs to use use JIRA REST API from Groovy, here are a couple tips that will make your life easier.

A few things you'll need to get started:

import groovyx.net.http.RESTClient;
import groovyx.net.http.HttpResponseDecorator;
import org.apache.http.HttpRequest;
import org.apache.http.protocol.HttpContext;
import org.apache.http.HttpRequestInterceptor;
import groovy.json.JsonSlurper;
import static groovyx.net.http.Method.*
import static groovyx.net.http.ContentType.*

@Grab(value = 'org.codehaus.groovy:groovy-all:1.7.5',
      initClass = false)
@Grapes([
@Grab(group = 'org.codehaus.groovy.modules.http-builder',
      module = 'http-builder', version = '0.5.1'),
@GrabExclude('org.codehaus.groovy:groovy')
])

The GET request for one was as easy as expected:

def jiraApiUrl = 'http://your-jira.com/rest/api/2/'
def jiraClient = new RESTClient(jiraApiUrl);
def serverInfo = jiraClient.get(path: 'serverInfo')

POST was bit harder. What you see below works, but trying any other way resulted in a NPE because of the response type settings. This is due to the fact that the HTTPClient (super class of the RESTClient) has only a few different response types registered by default and if an unknown type is provided, it will not know how to set the body. Instead of telling you about the issue nicely, it simply throws a NPE at you.

def uriPath = 'search'
// to start w/ something simple, get all new last week issues
def params = [maxResults : 250, jql : 'createdDate >= -7d']
def lastWeekIssues = jiraClient.post(requestContentType : JSON,
                                     path : uriPath,
                                     body : params)

Authentication was also a bit cumbersome. The simple authentication often used with groovy didn't work:

jiraClient.auth.basic 'http:/your-jira.com', 80, 
                      'yourUser', 'yourPassword'

As the JIRA documentation explains, it will not send an authentication challenge, instead the user of the API is required to provide it. The code below shows how to get away with Basic authentication. Be careful when doing so against some public JIRA server, you might be sending your login details out to someone you don't know. So when accessing a JIRA server on a public network instead of in an office environment, you might want to choose some other types of authentication such as OAuth, which is also supported by JIRA.

def basic = 'Basic ' + 
'yourUser:yourPassword'.bytes.encodeBase64().toString()
jiraClient.client.addRequestInterceptor(
  new HttpRequestInterceptor() {
    void process(HttpRequest httpRequest,
                 HttpContext httpContext) {
      httpRequest.addHeader('Authorization', basic)
  }
})

In the example above, we just hardcode the username and password. This is not so great if you want to commit this script in a repo and share it between multiple users. So in-house we use another function to retrieve the user name and password dynamically. You should do something like this as well. One thing to be aware of when working with an HTTPClient (or its subclass RESTClient) is that it doesn't really like dynamic strings. It looks like GROOVY-5761. Until this issue is resolved, you better stick to classic string concatenation.

// using dynamic string seems to run into
// http://jira.codehaus.org/browse/GROOVY-5761:
def login = "${cfg('adminUsername')}:${cfg('adminPassword')}"
def basic = "Basic " + login.bytes.encodeBase64().toString()

// so stick to string concatenation
def login = cfg('adminUsername') + ':' + cfg('adminPassword')
def basic = "Basic " + login.bytes.encodeBase64().toString()
httpRequest.addHeader('Authorization', basic)

When parsing results, I've found that the JSON parser used by the RESTClient is net.sf.json.JSON. I have also found that it is not as good in parsing responses from JIRA as one would expect. Groovy's own JsonSlurper is able to deal with a response returned by JIRA much better, so I ended up using that instead of the default. I'm pretty sure there is an even better way to achieve this, though: you could write a custom response decorator and get rid of net.sf.json.JSON before it's even used to parse a raw response. If anyone does this bit instead of using the code below, please share it in the comment section of this post.

def slurpedLastWeeksIssues = 
  new JsonSlurper().parseText(lastWeeksIssues.data.toString())

And, to finish this mini-tutorial, a few extra bits of advice: when searching, know what you are looking for. Limit the number of results returned by the query, especially when executing multiple searches as part of one script. Also know that if you don't specify how many results should be returned from the query, JIRA will return the default number of results configured for any given instance (50 in our case). So if you happen to want to process all the results returned from the query, you need to either keep re-executing the query with a higher startAt value or you need to increase maxResults to a number that ensures you get them all in one go.

Enjoy.