Atlassian Minecraft JIRA Plugin

It all started as a bit of a “joke” ShipIt project. A few of us play Minecraft together on a multi-player server and, after some customary ShipIt pizza and beer, decided that a Minecraft mod for creating and resolving JIRA issues would be a shoe-in for a crowd-pleasing ShipIt winner. Given that, if taken in proportion, the size of a Minecraft world is roughly equivalent to eight times the surface of the Earth, we all agreed it’s pretty easy to lose track of all the great ideas you get while playing. What better way to keep track of all the things you want to build in Minecraft than turning them into JIRA issues? I promptly threw away all the work I had done so far, and spent the rest of the night in the Atlassian office learning how to write Minecraft plugins and hacking up a very simple integration. I briefly considered the comfy-looking couch downstairs near the support team (it even has a blanket and a pillow on it), but I threw in the towel at around 2:00AM and caught a taxi home!

The Minecraft project made into the ShipIt XVIII finals, but Confluence’s brilliant new feature, “AutoConvert“, took first place honours in the end. From there, the JIRA team approached me to turn my ShipIt entry into a video we could use to promote the upcoming JIRA 5 launch. That turned out pretty well, too. So, here’s a quick run-down of how it all came together from the technical side.

Building in Bukkit

I used the Bukkit Minecraft Server to build the JIRA plugin. Bukkit is a community-powered project to build a stable, mod-able Minecraft server over the top of the official multi-player server daemon.

Bukkit has a nicely-structured API for building plugins, and if, like me, you’re familiar with Atlassian plugins (Java + Maven), you’ll feel right at home. At its core, the Bukkit API allows you to register commands for users to invoke via the in-game chat panel (eg. “/jiraIssues” to list all unresolved JIRA issues). There’s also an event-driven system that allows plugins to listen for certain events that occur within the in-game world and take action.

For example, the JIRA plugin needs to be informed whenever a new sign is crafted and placed within the in-game world and, if the sign contains the text “{jira}”, trigger a new JIRA issue to be created. The skeleton code for this is really straight-forward:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package au.id.jaysee.minecraft;

import org.bukkit.event.block.BlockListener;

public class McJiraBlockListener extends BlockListener
{
    @Override
    public void onSignChange(SignChangeEvent event)
    {
        super.onSignChange(event);

        if (event.getLine(0).equalsIgnoreCase("{jira}")
        {
            // TODO: Create new JIRA issue.
        }
    }
}

Like most computer games, Minecraft has an internal, endless loop that calculates the current state of the game world and then draws the three-dimensional world on the screen. The loop executes over and over, as fast as it can, to provide a near real-time “view” to the player. The faster the loop can run and re-draw the world environment, the more frames of video it can provide in a single second – the all-important, I-spend-all-day-tuning-my-video-card, check-out-my-benchmarks-on-tumblr FPS (frames per second). Many people are familiar with the maxim that 30 frames per second is the comfortable minimum required in order to trick the human eye into perceiving a live scene, instead of a series of static images super-imposed over each other.

Why is this important? Well, consider this loop is a single thread and any custom code you write in your plugin will execute on this thread… synchronously. Now consider what might happen if the JIRA server is temporarily unavailable and the HTTP call to create the issue only times out after thirty seconds. That’s right! You’ve just dropped the performance of the main loop down to 0.03 frames per second! Performance is always an important factor in any software application. In gaming, however, performance is crucial. Some slow running code that causes the screen to jitter or “lag” can shatter the immersive experience of the game. It’s like having someone spill popcorn on you in the movie theatre just as Darth Vader reveals the horrible truth to Luke Skywalker – the experience is totally ruined!

Multi-threading for Performance

Bukkit’s API has a “Scheduler” component that allows you to offload expensive operations onto a background thread. As a general rule, anything that does not interact directly with the in-game environment (and thus, needs to be synchronised for thread safety) should be done off the main thread. Using the Bukkit API, two calls are typically made. The first one, to offload some processing onto a background thread and the second one to return some result back onto the main thread to update the world. To be honest, I found the Bukkit API for this a bit cumbersome so I wrapped the calls in a helper class that makes it easy to define both behaviours in one go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* Provides a convenient method for executing an asynchronous task on a background thread with a callback that is
* automatically marshalled back on to the main thread when the async task is complete.
*
* @param task The code to be executed in the background.
* @param callback The callback to be executed in the foreground.
* @param The type parameter specifies the type of the value returned from the {@link au.id.jaysee.minecraft.task.Task#execute()} method,
* which is passed into the {@link Callback#execute(Object)} method as the input parameter.
*/

public [T] void executeAsyncTask(final Task task, final Callback callback)
{
        bukkitScheduler.scheduleAsyncDelayedTask(myPlugin, new Runnable() // Execute this Runnable on a background thread at some time in the future, and return immediately.
        {
            @Override
            public void run()
            {
                final T result = task.execute();
                scheduler.scheduleSyncDelayedTask(myPlugin, new Runnable() // Execute this Runnable back on the foreground thread.
                {
                    @Override
                    public void run()
                    {
                        callback.execute(result);
                    }
                });
            }
         });
}

Using Java’s support for anonymous interface implementations, using this scheduler wrapper makes it easy to define the task and the callback within a single method where they can share immutable configuration and initial state:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
taskExecutor.executeAsyncTask(new Task[JiraIssues]()
{
    @Override
    public JiraIssues execute()
    {
        return jiraClient.getIssues();
    }
    }, new Callback[JiraIssues]()
    {
        @Override
        public void execute(JiraIssues input)
        {
            Player p = (Player)sender;
            for (JiraIssue j : input.getIssues())
            {
                 p.chat(j.getKey() + ": " + j.getSummary());
            }
        }
    });
}

Building the REST

With the Bukkit plugin taking shape, communicating with JIRA via the new and improved JIRA 5 REST API was pleasantly easy. I was initially skeptical, having much more experience with SOAP Web Services and having been burned by poorly-structured and poorly-documented REST APIs in the past. Fortunately those JIRA guys totally nailed it, and the REST API rocks! The API documentation, even in its beta form at the time, was tremendously helpful. I was also able to bundle Jersey and Jackson libraries into my Minecraft plugin which, combined, turned the massaging of data in and out of my plugin into a “simple” matter of API glue. :-)

The “/jiraIssues” command, for example, which prints the list of open JIRA issues to the Minecraft chat window, was almost a one-liner in my first implementation! Getting the list of issues was just a matter of making a GET request like this (admittedly, JQL as a query parameter is not pretty):

1
/rest/api/2/search?maxResults=10&jql=project%20%3D%20MC%20%26%20resolution%20%3D%20unresolved

The returned JSON blob gets deserialized into a JSONObject using Jackson and then, as all I needed was the issue key and summary, left as-is.

Unlike a SOAP-based Web Service, this kind of ‘roll-your-own’ client-side resources can be a bit cumbersome when you have to implement it over and over again – especially in a static language like Java. Fortunately, we have a solution for that – the JIRA REST Java Client Library is under active development, is backwards compatible with JIRA 4.x and does all the hard work for you. I would definitely recommend trying out the JRJC before rolling your own JIRA REST client.

Making The Video

The hardest part for me, but also the most fun, was putting together the “demo” video for the plugin. Having no experience with any kind of video editing I set aside a day to put it all together using iMovie. I was about three hours in to my recording and editing marathon before I realised that all video editing software is terrible, incompatible video codecs were invented purely for the torture of humankind and transferring video files around two different computers is much easier by plugging an un-plugging a USB disk drive than it is to try and setup a home network between a Windows PC and a Macbook.

In the end, this process worked for me:

  1. Spend about forty-five minutes constructing a giant Atlassian logo out of Minecraft blocks
  2. Record all raw Minecraft footage on my Windows PC using FRAPS
  3. Transcode all the raw footage from the proprietary fraps video codec to MPEG-4 using VirtualDub
  4. Transfer MP4 footage to MacBook using external USB 2.0 disk drive
  5. Import footage into iMovie
  6. Grab a beer
  7. Spend hours and hours fiddling with frames of footage, arranging them into what you dream to be an Academy-award winning sequence
  8. Upload to youtube (At least this part was easy!)

Get the Plugin

If you want to get to the plugin itself, it’s available for download from plugins.atlassian.com. The source is also publicly available on my bitbucket repo, and I am happy to accept both bug reports and pull requests.