Release Notes with Groovy and JIRA REST API: Part II – Separate responsibilities

A while ago I described how to get release notes from JIRA with Groovy, so I thought it’s time to do a small follow up which is long overdue :-).

Let’s split things up to manageable classes with their own responsibility:

  • a JIRA REST client which encapsulates connecting to my JIRA instance, such as (HTTP) getting or posting from and to an url while using the proper credentialsgroovy-logo-medium
  • a JIRA query builder, responsible for building JQL snippets to be used for the JIRA search REST API

We’ll start at the bottom, so we’ll build upon the classes we already have.

Just Being Typesafe

Although a lot of the date we’re getting back from JIRA can be used as-is in Groovy, I decided to make the status of a JIRA issue, e.g. “Open”, into a trivial enum.

enum JiraStatus {
    OPEN("Open"), IN_PROGRESS("In Progress"), RESOLVED("Resolved"), REOPENED("Reopened"), CLOSED("Closed");

    JiraStatus(String value) { this.value = value }
    private final String value
    String value() { return value }
}

I’ll stick to Strings in the remainder of this post 🙂

Building a JQL Query

Queries you can execute within JIRA itself in the Issue Navigator can also be performed through the REST API. Because there are several elements which constitute a JQL-query I decided to use the builder pattern to programmatically create queries through the JiraQueryBuilder.

import groovyx.net.http.RESTClient
import JiraStatus

/**
 * Builder for a JQL-snippet, used for the JIRA search REST API.
 */
class JiraQueryBuilder  {

    private String project
    private int[] sprintIds
    private String sprintFunction
    private String[] issueKeys
    private String[] issueTypes
    private String[] excludedIssueTypes
    private String[] resolution
    private JiraStatus[] status
    private String[] labels
    private String fixVersion
    private String orderBy

    /**
     * Create a new query builder, querying by default everything for specified project, ordered by key.
     */
    public JiraQueryBuilder(String project) {
        assert project
        this.project = project
        withIssueTypes("standardIssueTypes()")
        withOrderBy("key")
    }

    JiraQueryBuilder withSprintIds(int...sprintIds) {
        this.sprintIds = sprintIds
        return this
    }

    JiraQueryBuilder withIssueKeys(String...issueKeys) {
        this.issueKeys = issueKeys
        return this
    }

    JiraQueryBuilder withIssueTypes(String... issueTypes) {
        this.issueTypes = issueTypes
        return this
    }

    JiraQueryBuilder withExcludedIssueTypes(String... excludedIssueTypes) {
        this.excludedIssueTypes = excludedIssueTypes
        return this
    }

    JiraQueryBuilder withResolution(String... resolution) {
        this.resolution = resolution
        return this
    }

    JiraQueryBuilder withStatus(JiraStatus... status) {
        this.status = status
        return this
    }

    JiraQueryBuilder withLabels(String... labels) {
        this.labels = labels
        return this
    }

    JiraQueryBuilder withFixVersion(String fixVersion) {
        this.fixVersion = fixVersion
        return this
    }

    JiraQueryBuilder withOrderBy(String orderBy) {
        this.orderBy = orderBy
        return this
    }

    String build() {
        String jql = "project = ${project}"

        if (sprintIds) {
            jql += " AND sprint in (${sprintIds.join(',')})"
        }

        if (sprintFunction) {
            jql += " AND sprint in ${sprintFunction}"
        }

        if (issueKeys) {
            jql += " AND issuekey in (${issueKeys.join(',')})"
        }

        if (issueTypes) {
            jql += " AND issuetype in (${issueTypes.join(',')})"
        }

        if (excludedIssueTypes) {
            jql += " AND issuetype not in (${excludedIssueTypes.join(',')})"
        }

        if (status) {
            // turn (IN_PROGRESS, RESOLVED) into (Resolved, In Progress)
            jql += " AND status in (${status*.value().join(',')})"
        }

        if (resolution) {
            jql += " AND resolution in (${resolution.join(',')})"
        }

        if (labels) {
            jql += " AND labels in (${labels.join(',')})"
        }

        if (fixVersion) {
            jql += " AND fixVersion = \"${fixVersion}\""
        }

        if (orderBy) {
            jql += " order by ${orderBy}"
        }

        return jql
    }
}

Connecting to JIRA

Finally I needed to encapsulate the connection to a JIRA instance, allowing me to (HTTP) GET and POST from and to it, including using the proper credentials.

// require(groupId:'org.codehaus.groovy.modules.http-builder', artifactId:'http-builder', version:'0.5.2')
import net.sf.json.groovy.*
import groovyx.net.http.RESTClient
import static JiraStatus.*

/**
 * JIRA REST client wrapper around groovyx.net.http.RESTClient
 */
class JiraRESTClient extends groovyx.net.http.RESTClient {

    private static final String DEFAULT_PROJECT = "XXX"
    private static final String DEFAULT_SEARCH_URL = "http://jira/rest/api/latest/"

    String username
    String password
    def credentials = [:]

    /**
     * Create a REST client using Maven properties jira.username and jira.password for JIRA username and password.
     */
    static JiraRESTClient create(def project) {

        String jiraUsername = project.properties['jira.username']
        String jiraPassword = project.properties['jira.password']

        if (!jiraUsername?.trim()) {
            throw new IllegalArgumentException("Empty property: jira.username")
        }

        if (!jiraPassword?.trim()) {
             throw new IllegalArgumentException("Empty property: jira.password")
        }

        return create(jiraUsername, jiraPassword)
    }

    /**
     * Create a REST client using provided JIRA username and password.
     */
    static JiraRESTClient create(String username, String password) {
        return new JiraRESTClient(DEFAULT_SEARCH_URL, username, password)
    }

    private JiraRESTClient(String url, String username, String password) {
        super(url)
        assert username
        assert password

        log.debug "Created for user=${username}, url=" + url
        this.username = username;
        this.password = password;

        credentials['os_username'] = this.username
        credentials['os_password'] = this.password
    }

    def get(String path, def query) {

        try {
            def response = get(path: path, contentType: "application/json", query: query)
            assert response.status == 200
            assert (response.data instanceof net.sf.json.JSON)
            return response
        } catch (groovyx.net.http.HttpResponseException e) {
            if (e.response.status == 400) {
                // HTTP 400: Bad Request, JIRA returned error
                throw new IllegalArgumentException("JIRA query failed, response data=${e.response.data}", e)
            } else {
                throw new IOException("JIRA connection failed, got HTTP status ${e.response.status}, response data=${e.response.data}", e)
            }

        }
    }

    def post(String path, def body, def query) {

        try {
            def response = post(path: path, contentType: "application/json", body: body, query: query)
            return response
        } catch (groovyx.net.http.HttpResponseException e) {
            if (e.response.status == 400) {
                // HTTP 400: Bad Request, JIRA returned error
                throw new IllegalArgumentException("JIRA query failed, got HTTP status 400, response data=${e.response.data}", e)
            } else {
                throw new IOException("JIRA connection failed, got HTTP status ${e.response.status}, response data=${e.response.data}", e)
            }

        }
    }

    /**
     * Search JIRA with provided JQL e.g. "project = XXX AND ..."
     * @returns response
     * @throws IllegalArgumentException in case of bad JQL query
     * @throws IOException in case of JIRA connection failure
     * @see JiraQueryBuilder to create these JQL queries
     */
    def search(String jql) {
        assert jql

        def query = [:]
        query << credentials
        query['jql'] = jql

        query['startAt'] = 0
        query['maxResults'] = 1000

        log.debug "Searching with JQL: " + jql
        return get("search", query)
    }

Credentials are appended in plain sight to the url, but for internal use I found this acceptable. The search() gets a JQL string passed, appends the credentials and delegates to our own get() which actually performs groovyx.net.http.RESTClient‘s get() to make it happen.

Querying Release Notes

The query to get all Resolved or Closed issues for a certain fix version  can be encapsulated in a getReleaseNotes method, such as seen below.

def getReleaseNotes(String fixVersion) {
  String jql = new JiraQueryBuilder(DEFAULT_PROJECT)
                    .withStatus(RESOLVED, CLOSED)
                    .withFixVersion(fixVersion)
                    .build()
  return search(jql)
}

Done

I’m still kickstarting this script with e.g. the gmaven-plugin which allows me to get my JIRA credentials from e.g. settings.xml, while the fix version for the JIRA query can be supplied as a property on the command-line.

Grails domain classes and special presentation requirements

In Grails we often use our domain objects directly as backing model for presentation purposes and only for specialized situations we create value holder objects or DTOs.

Beginning Grails developers know how to display individual entity properties pretty easy in a GSP, such as the name of a fictional domain class…

<g:each in=”${breedingGoals}” var=”breedingGoal”>
  ${breedingGoal.name}
</g:each>

or

<g:select from="${breedingGoals}"
  optionKey="code" 
  optionValue="name" />

breeding-goal-name

but sometimes struggle when new presentational requirements come in and slightly more or combined information needs to be displayed. Special constructions are contrived, such as the following:

def list() {
  def breedingGoals = BreedingGoal.list()
  breedingGoals.each {
    it.name = it.name + " ("+ it.code + ")"
  }
  render view: “viewName”, model: [breedingGoals: breedingGoals]
}

to not only display the name any more, but also something else – e.g. its code. And that’s when things get hairy 🙂

breeding-goal-name-and-code

This looks like a sensible way to get the new correct output in the browser. But it is not.

In this case entities are actually updated and leave the database in a very messed up state after a few iterations. This is the point where seasoned developers tell beginning developers not to continue down that road.

If you are one of the latter, read some of GORM’s persistence basics for some background first, and get back here for some guidelines you could use, in order of my personal preference.

My main rule of thumb is:

Do NOT replace data by a (re)presentational version of itself.

It’s not a fully comprehensive list, just to get you thinking about a few options taking our aforementioned BreedingGoal use case as an example.

If the information is contained within the object itself 

and you have the object, just format it right on the spot.

<g:each in=”${breedingGoals}” var=”breedingGoal”>
  ${breedingGoal.name} (${breedingGoal.code})
</g:each>

A GSP is especially useful for presentation purposes. I would always try that first.

Some people still get confused of the various Grails tags which seem to accept “just 1 property” of the domain class being iterated over, such as select

<g:select from="${breedingGoals}"
  optionKey="code" 
  optionValue="name + (code)? Arh! Now what?" />

Luckily, sometimes the tag author thought about this 🙂 In the case of select, optionValue can apply a transformation using a closure:

<g:select from="${breedingGoals}"
  optionKey="code" 
  optionValue="${{ it.name + ' (' + it.code + ')' }}"  />

If this new formatted information is needed on more places

to stay DRY you can put it in the object itself. In your domain class, add a getter:

String getNamePresentation() {
  name + " ("+ code + ")"
}

which can be called as it were a property of the object itself.

<g:each in=”${breedingGoals}” var=”breedingGoal”>
  ${breedingGoal.namePresentation}
</g:each>

If information is NOT in the object itself, but needs to be retrieved/calculated with some other logic

you can use a Tag Library.

class BreedingGoalTagLib {

  def translationService

  def formatBreedingGoal = { attrs ->
    out << translationService.translate(attrs.code, user.lang)
  }
}

In this example some kind of translationService is used to get the actual information you want to display.

<g:each in=”${breedingGoals}” var=”breedingGoal”>
  <g:formatBreedingGoal code=${breedingGoal.code} />
</g:each>

A TagLib is very easy to call from a GSP and has full access to collaborating services and the Grails environment.

If there’s no additional calculation necessary, but just special (HTML) formatting needs

Get the stuff and delegate to a template:

class BreedingGoalTagLib {

  def displayBreedingGoal = { attrs ->
    out << render(template: '/layouts/breedingGoal', 
      bean: attrs.breedingGoal)
  }
}

where the specific template could contain some HTML for special markup:

<strong>${it.name}</strong> (${it.code})

Or you need to initialize a bunch of stuff once or you’re not really aiming for GSP display

you could create a (transient) property and fill it upon initialization time

Adjust the domain class, make your new “presentation” property transient – so it won’t get persisted to the database – and initialize it at some point.

class BreedingGoal {

  String code
  String name
  String namePresentation

  static transients = ['namePresentation']
}

class SomeInitializationService {

  // in some method...
  def breedingGoal = findBreedingGoal(...
  breedingGoal.namePresentation = 
    translationService.translate(breedingGoal.code, user.lang)

  return breedingGoal

}

Friendly word of advice: don’t try and litter your domain classes too much with transient fields for presentation purposes. If your domain class contains 50% properties with weird codes and you need the other half with associated “presentation” fields, you might be better off with a specialized value or DTO object.

Conclusion

You see there a various presentation options and depending on where the information comes from and where it goes to some approaches are better suited than others. Don’t be afraid to choose one and refactor to another approach when necessary, but I’d stick to the simplest option in the GSP first and try not to mangle your core data for presentation purposes too much 🙂

I welcome additional tips in the comments!

Grails GORM – You Know SQL. You Know Queries. Here’s GORM.

Today I gave a presentation about Grails Object Relational Mapping (GORM) and HQL for the teams.

Everybody is pretty SQL-minded and I wanted to show them what — instead of resorting to native SQL in Grails applications right away — GORM and Hibernate can do for them, not just from getting the same queries as your own hand-crafted SQL but also showing them that readability and testability should also be first-class citizens.

View it on SlideShare or embedded below: