JIRA: Basic C#/Jira searching for issues

(This article is a follow up to http://maffelu.net/jira-basic-cjira-fetching-and-displaying-projects/)

In the previous article, and the article before that, we talked about creating a connection to JIRA with C# and using that connection to get all projects. In this article I will explain how to get all issues for a project.

First of all we’ll be using the code from previous articles, so we’ll build a class called JiraManager which will, for brevity, be handling all our communication. It has one protected method called RunQuery which runs a query against your JIRA instance. It handles the encryption of credentials and sending/fetching of data. All other methods (such as GetProjects and GetIssues) will be calling this method with different parameters:

 

 

public enum JiraResource
{
    project,
    search
}
 
public class JiraManager
{
    private const string m_BaseUrl = "http://localhost.:8080/rest/api/latest/";
    private string m_Username;
    private string m_Password;
 
    public JiraManager(string username, string password)
    {
        m_Username = username;
        m_Password = password;
    }
 
    /// <summary>
    /// Runs a query towards the JIRA REST api
    /// </summary>
    /// <param name="resource">The kind of resource to ask for</param>
    /// <param name="argument">Any argument that needs to be passed, such as a project key</param>
    /// <param name="data">More advanced data sent in POST requests</param>
    /// <param name="method">Either GET or POST</param>
    /// <returns></returns>
    protected string RunQuery(
        JiraResource resource, 
        string argument = null, 
        string data = null,
        string method = "GET")
    {
        string url = string.Format("{0}{1}/", m_BaseUrl, resource.ToString());
 
        if (argument != null)
        {
            url = string.Format("{0}{1}/", url, argument);
        }
 
        HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
        request.ContentType = "application/json";
        request.Method = method;
 
        if (data != null)
        {
            using (StreamWriter writer = new StreamWriter(request.GetRequestStream()))
            {
                writer.Write(data);
            }
        }
 
        string base64Credentials = GetEncodedCredentials();
        request.Headers.Add("Authorization", "Basic " + base64Credentials);
 
        HttpWebResponse response = request.GetResponse() as HttpWebResponse;
 
        string result = string.Empty;
        using (StreamReader reader = new StreamReader(response.GetResponseStream()))
        {
            result = reader.ReadToEnd();
        }
 
        return result;
    }
 
    public List<ProjectDescription> GetProjects()
    {
        List<ProjectDescription> projects = new List<ProjectDescription>();
        string projectsString = RunQuery(JiraResource.project);
 
        return JsonConvert.DeserializeObject<List<ProjectDescription>>(projectsString);
    }
 
    public List<Issue> GetIssues(
        string jql, 
        List<string> fields = null,
        int startAt = 0, 
        int maxResult = 50)
    {
        fields = fields ?? new List<string>{"summary", "status", "assignee"};
 
        SearchRequest request = new SearchRequest();
        request.Fields = fields;
        request.JQL = jql;
        request.MaxResults = maxResult;
        request.StartAt = startAt;
 
        string data = JsonConvert.SerializeObject(request);
        string result = RunQuery(JiraResource.search, data: data, method: "POST");
 
        SearchResponse response = JsonConvert.DeserializeObject<SearchResponse>(result);
 
        return response.IssueDescriptions;
    }
 
    private string GetEncodedCredentials()
    {
        string mergedCredentials = string.Format("{0}:{1}", m_Username, m_Password);
        byte[] byteCredentials = UTF8Encoding.UTF8.GetBytes(mergedCredentials);
        return Convert.ToBase64String(byteCredentials);
    }
}

We’ve added a resource to the JiraResource enumeration. This is the ‘search’ resource. The rest call to search for issues looks like this:
/rest/api/latest/search

So a call might look like this:
http://localhost:8080/rest/api/latest/search

In order to search we need to pass along a search request with the post data to the rest service. The data could look like this:

{
    "jql": "project = JSHARP",
    "startAt": 0,
    "maxResults": 15,
    "fields": [
        "summary",
        "status",
        "assignee"
    ]
}

So, we’ll create a class to represent this search request (as can be seen used in the GetIssues method in the code above):

public class SearchRequest
{
    [JsonProperty("jql")]
    public string JQL { get; set; }
 
    [JsonProperty("startAt")]
    public int StartAt { get; set; }
 
    [JsonProperty("maxResults")]
    public int MaxResults { get; set; }
 
    [JsonProperty("fields")]
    public List<string> Fields { get; set; }
 
    public SearchRequest()
    {
        Fields = new List<string>();
    }
}

This allows us to create the search request and later on serialize it to JSON and pass it on in the request. The return result can look like this:

{
	"expand" : "schema,names",
	"startAt" : 0,
	"maxResults" : 50,
	"total" : 1,
	"issues" : [{
			"expand" : "editmeta,renderedFields,transitions,changelog,operations",
			"id" : "10004",
			"self" : "http://localhost.:8080/rest/api/latest/issue/10004",
			"key" : "JSHARP-3",
			"fields" : {
				"summary" : "Install visual studio 2010 express",
				"status" : {
					"self" : "http://localhost.:8080/rest/api/2/status/5",
					"description" : "A resolution has been taken, and it is awaiting verification by reporter. From here issues are either reopened, or are closed.",
					"iconUrl" : "http://localhost.:8080/images/icons/status_resolved.gif",
					"name" : "Resolved",
					"id" : "5"
				},
				"assignee" : {
					"self" : "http://localhost.:8080/rest/api/2/user?username=adm",
					"name" : "adm",
					"emailAddress" : "foo@bar.com",
					"avatarUrls" : {
						"16x16" : "http://localhost.:8080/secure/useravatar?size=small&avatarId=10122",
						"48x48" : "http://localhost.:8080/secure/useravatar?avatarId=10122"
					},
					"displayName" : "Magnus Ferm",
					"active" : true
				}
			}
		}
	]
}

This result can in turn be represented by C# classes as well so that we can deserialize it and use it natively:

public class SearchResponse
{
    [JsonProperty("expand")]
    public string Expand { get; set; }
 
    [JsonProperty("startAt")]
    public int StartAt { get; set; }
 
    [JsonProperty("maxResults")]
    public int MaxResults { get; set; }
 
    [JsonProperty("total")]
    public int Total { get; set; }
 
    [JsonProperty("issues")]
    public List<Issue> IssueDescriptions { get; set; }
}

This response contains a list of issues, they are built up like this:

public class Issue : BaseEntity
{
    private string m_KeyString;
 
    [JsonProperty("expand")]
    public string Expand { get; set; }
 
    [JsonProperty("id")]
    public int Id { get; set; }
 
    #region Special key solution
    [JsonProperty("key")]
    public string ProxyKey
    {
        get
        {
            return Key.ToString();
        }
        set
        {
            m_KeyString = value;
        }
    }
 
    [JsonIgnore]
    public IssueKey Key
    {
        get
        {
            return IssueKey.Parse(m_KeyString);
        }
    }
    #endregion Special key solution
 
    [JsonProperty("fields")]
    public Fields Fields { get; set; }
}
 
/// <summary>
/// A class representing a JIRA issue key [PROJECT KEY]-[ISSUE ID]
/// </summary>
public class IssueKey
{
    public string ProjectKey { get; set; }
 
    public int IssueId { get; set; }
 
    public IssueKey() { }
    public IssueKey(string projectKey, int issueId)
    {
        ProjectKey = projectKey;
        IssueId = issueId;
    }
 
    public static IssueKey Parse(string issueKeyString)
    {
        if (issueKeyString == null)
        {
            throw new ArgumentNullException("IssueKeyString is null!");
        }
 
        string[] split = issueKeyString.Split('-');
 
        if (split.Length != 2)
        {
            throw new ArgumentException("The string entered is not a JIRA key!");
        }
 
        int issueId = 0;
        if (!int.TryParse(split[1], out issueId))
        {
            throw new ArgumentException("The string entered could not be parsed, issue id is non-integer!");
        }
 
        return new IssueKey(split[0], issueId);
    }
 
    public override string ToString()
    {
        return string.Format("{0}-{1}", ProjectKey, IssueId);
    }
}
 
public class Fields
{
    [JsonProperty("summary")]
    public string Summary { get; set; }
 
    [JsonProperty("status")] 
    public Status Status { get; set; }
 
    [JsonProperty("assignee")]
    public Assignee Assignee { get; set; }
}
 
public class Status : BaseEntity
{
    [JsonProperty("description")]
    public string Description { get; set; }
 
    [JsonProperty("iconUrl")]
    public string IconUrl { get; set; }
 
    [JsonProperty("name")]
    public string Name { get; set; }
 
    [JsonProperty("id")]
    public int Id { get; set; }
}
 
public class Assignee : BaseEntity
{
    [JsonProperty("name")]
    public string Name { get; set; }
 
    [JsonProperty("emailAddress")]
    public string EmailAddress { get; set; }
 
    [JsonProperty("avatarUrls")]
    public AvatarUrls AvatarUrls { get; set; }
 
    [JsonProperty("displayName")]
    public string DisplayName { get; set; }
 
    [JsonProperty("active")]
    public bool Active { get; set; }
}
 
public class AvatarUrls
{
    [JsonProperty("16x16")]
    public string Size16 { get; set; }
 
    [JsonProperty("48x48")]
    public string Size48 { get; set; }
}

This is how we represent the return result as objects in C#. This became a pretty huge example so I’ve decided to upload the source code for this example:
JiraExample.zip

If you have any questions just put them in the comments for this article 🙂

JIRA: Basic C#/JIRA fetching and displaying projects

(This is a follow-up to the initial JIRA/C# article: “JIRA: Basic C#/JIRA connection using REST“)

In the previous article about how to work with JIRA in C# utilizing the REST api we made a connection between a console application and JIRA. In this article I will show you how to query for projects and deserialize them using the JSON.NET library.

First off, download the JSON.NET library here.
We will continue on our initial project so I will only explain the new sections (and only if they need explanation). I apologize in advance for the none-refactored code but this is just an example.

Now, the JIRA REST api always replies with data in JSON format. This means we can create C# classes and serialize the data into usable objects. If we look at the result from the first article when we asked for all articles:

[{"self":"http://localhost.:8080/rest/api/2/project/JSHARP","id":"10001","key":"JSHARP","name":"Jira#","avatarUrls":{"16x16":"http://localhost.:8080/secure/projectavatar?size=small&pid=10001&avatarId=10002","48x48":"http://localhost.:8080/secure/projectavatar?pid=10001&avatarId=10002"}},{"self":"http://localhost.:8080/rest/api/2/project/TP","id":"10000","key":"TP","name":"Test Project","avatarUrls":{"16x16":"http://localhost.:8080/secure/projectavatar?size=small&pid=10000&avatarId=10011","48x48":"http://localhost.:8080/secure/projectavatar?pid=10000&avatarId=10011"}}]

I have two projects, Jira# and Test Project. If we copy this into Notepad++ and use the JSMin plugin we’ll see the following:

[{
		"self" : "http://localhost.:8080/rest/api/2/project/JSHARP",
		"id" : "10001",
		"key" : "JSHARP",
		"name" : "Jira#",
		"avatarUrls" : {
			"16x16" : "http://localhost.:8080/secure/projectavatar?size=small&pid=10001&avatarId=10002",
			"48x48" : "http://localhost.:8080/secure/projectavatar?pid=10001&avatarId=10002"
		}
	}, {
		"self" : "http://localhost.:8080/rest/api/2/project/TP",
		"id" : "10000",
		"key" : "TP",
		"name" : "Test Project",
		"avatarUrls" : {
			"16x16" : "http://localhost.:8080/secure/projectavatar?size=small&pid=10000&avatarId=10011",
			"48x48" : "http://localhost.:8080/secure/projectavatar?pid=10000&avatarId=10011"
		}
	}
]

(If you don’t have the JSMin plugin in Notepad++ just go to Plugins => Plugin Manager => Show Plugin Manager => Available and scroll down to JSMin and press install. To format then select Plugins => JSMin => JSFormat)

We’ll create two classes (one for each project and one for the property AvatarUrls):

ProjectDescription

public class ProjectDescription : BaseEntity
{
    [JsonProperty("id")]
    public int Id { get; set; }
 
    [JsonProperty("key")]
    public string Key { get; set; }
 
    [JsonProperty("name")]
    public string Name { get; set; }
 
    [JsonProperty("avatarUrls")]
    public AvatarUrls AvatarUrls { get; set; }
}

AvatarUrls

public class AvatarUrls
{
    [JsonProperty("16x16")]
    public string Size16 { get; set; }
 
    [JsonProperty("48x48")]
    public string Size48 { get; set; }
}

As you can see we use the JsonPropertyAttribute to decorate each property with their real name. Some of the properties are poorly named so we’ll want to change the names in our classes. Now for the deserialization process. In the previous article we created a class called JiraManager which handled all the communication, we’ll continue on that one and rebuild it a bit:

public enum JiraResource
{
    project
}
 
public class JiraManager
{
    private const string m_BaseUrl = "http://localhost.:8080/rest/api/latest/";
    private string m_Username;
    private string m_Password;
 
    public JiraManager(string username, string password)
    {
        m_Username = username;
        m_Password = password;
    }
 
    /// <summary>
    /// Runs a query towards the JIRA REST api
    /// </summary>
    /// <param name="resource">The kind of resource to ask for</param>
    /// <param name="argument">Any argument that needs to be passed, such as a project key</param>
    /// <param name="data">More advanced data sent in POST requests</param>
    /// <param name="method">Either GET or POST</param>
    /// <returns></returns>
    protected string RunQuery(
        JiraResource resource, 
        string argument = null, 
        string data = null,
        string method = "GET")
    {
        string url = string.Format("{0}{1}/", m_BaseUrl, resource.ToString());
 
        if (argument != null)
        {
            url = string.Format("{0}{1}/", url, argument);
        }
 
        HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
        request.ContentType = "application/json";
        request.Method = method;
 
        if (data != null)
        {
            using (StreamWriter writer = new StreamWriter(request.GetRequestStream()))
            {
                writer.Write(data);
            }
        }
 
        string base64Credentials = GetEncodedCredentials();
        request.Headers.Add("Authorization", "Basic " + base64Credentials);
 
        HttpWebResponse response = request.GetResponse() as HttpWebResponse;
 
        string result = string.Empty;
        using (StreamReader reader = new StreamReader(response.GetResponseStream()))
        {
            result = reader.ReadToEnd();
        }
 
        return result;
    }
 
    public List<ProjectDescription> GetProjects()
    {
        List<ProjectDescription> projects = new List<ProjectDescription>();
        string projectsString = RunQuery(JiraResource.project);
 
        return JsonConvert.DeserializeObject<List<ProjectDescription>>(projectsString);
    }
 
 
 
    private string GetEncodedCredentials()
    {
        string mergedCredentials = string.Format("{0}:{1}", m_Username, m_Password);
        byte[] byteCredentials = UTF8Encoding.UTF8.GetBytes(mergedCredentials);
        return Convert.ToBase64String(byteCredentials);
    }
}

So what we simply do is that we deserialize the project using the JsonConvert class utilizing the classes we build for the incoming data. Use it like this:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello and welcome to a Jira Example application!");
 
        #region Create manager
        Console.Write("Username: ");
        string username = Console.ReadLine();
 
        Console.Write("Password: ");
        string password = Console.ReadLine();
 
        JiraManager manager = new JiraManager(username, password);
        #endregion
 
        Console.Clear();
 
        List<ProjectDescription> projects = manager.GetProjects();
        Console.WriteLine("Select a project: ");
        for (int i = 0; i < projects.Count; i++)
        {
            Console.WriteLine("{0}: {1}", i, projects[i].Name);
        }
 
        Console.Write("Project to open: ");
        string projectStringIndex = Console.ReadLine();
        int projectIndex = 0;
        if (!int.TryParse(projectStringIndex, out projectIndex))
        {
            Console.WriteLine("You failed to select a project...");
            Environment.Exit(0);
        }
 
        ProjectDescription selectedProject = projects[projectIndex];
        Console.WriteLine("You selected {0}", selectedProject.Name);
 
        Console.Read();
    }
}

The output should look like this:

Fetching projects from Jira and using them in C#
Fetching projects from Jira and using them in C#

In the next article we’ll look at how to search for issues: http://maffelu.net/jira-basic-cjira-searching-for-issues/.

JIRA: Basic C#/JIRA connection using REST

As I’m working a lot through JIRA I thought I’d try to communicate with it using C# and the REST api it’s exposing. This was not completely simple and it’s obvious they prefer people using java. There is a library available if you are a java user which simplifies a lot but there is none for C#. I will probably write more than a few articles about the subject as I’ve just installed JIRA on my computer.

The first thing you need to know about communicating with the REST api is that the full api documentation is found here. This will be your bestest friend ever when developing against it.
The second thing you need to know is that they used to have a SOAP api but since 4.something that’s deprecated in favor of the new REST api.

A standard JIRA REST call looks like this:
http://host:port/context/rest/api-name/api-version/resource-name

If you want to try it out you can use an open project on atlassian:
https://jira.atlassian.com/rest/api/latest/issue/JRA-9

The response is given in JSON format which is short and nice and easy to parse using the JSON.NET library. In this first article however we’ll just ensure that we can connect to JIRA and get some data in return.
When we use the open atlassian project in the link above we view the issue as an anonymous user. That takes us only so far so one of the most important parts of the connection is to send the proper credentials. We’ll create a console project which asks the user for a username and password which it uses to fetch all projects available to that user. That’s enough to get one started:

public enum JiraResource
{
    project
}
 
public class JiraManager
{
    private const string m_BaseUrl = "http://localhost.:8080/rest/api/latest/";
    private string m_Username;
    private string m_Password;
 
    public JiraManager(string username, string password)
    {
        m_Username = username;
        m_Password = password;
    }
 
    public void RunQuery(
        JiraResource resource, 
        string argument = null, 
        string data = null,
        string method = "GET")
    {
        string url = string.Format("{0}{1}/", m_BaseUrl, resource.ToString());
 
        if (argument != null)
        {
            url = string.Format("{0}{1}/", url, argument);
        }
 
        HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
        request.ContentType = "application/json";
        request.Method = method;
 
        if (data != null)
        {
            using (StreamWriter writer = new StreamWriter(request.GetRequestStream()))
            {
                writer.Write(data);
            }
        }
 
        string base64Credentials = GetEncodedCredentials();
        request.Headers.Add("Authorization", "Basic " + base64Credentials);
 
        HttpWebResponse response = request.GetResponse() as HttpWebResponse;
 
        string result = string.Empty;
        using (StreamReader reader = new StreamReader(response.GetResponseStream()))
        {
            result = reader.ReadToEnd();
        }
 
        Console.WriteLine(result);
    }
 
    private string GetEncodedCredentials()
    {
        string mergedCredentials = string.Format("{0}:{1}", m_Username, m_Password);
        byte[] byteCredentials = UTF8Encoding.UTF8.GetBytes(mergedCredentials);
        return Convert.ToBase64String(byteCredentials);
    }
}

And then we use it in our programs.cs file:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello and welcome to a Jira Example application!");
 
        Console.Write("Username: ");
        string username = Console.ReadLine();
 
        Console.Write("Password: ");
        string password = Console.ReadLine();
 
        JiraManager manager = new JiraManager(username, password);
        manager.RunQuery(JiraResource.project);
 
        Console.Read();
    }
}

The code could do with some refactoring but this is just a simple example. This, if run, should generate something like this (if you log in correctly and have some projects):

A finished connection to Jira
A finished connection to Jira with all Projects for ‘testuser’ showing.

Things to notice in the code:

  • The Authorization header that we use to send our credentials look like this: “Authorization” : “Basic [BASE64 ENCODED CREDENTIALS]”
  • Content-type must be application/json as we send and receive data using the json format
  • The RunQuery method is more generic than required but I’m going to re-use it in future articles. It allows for you to attach data and extra parameters which is sometimes needed
  • The URL goes to localhost. (notice the dot ‘.’ after localhost). This is to make the transaction visible in Fiddler when I monitor the communication

If you need to monitor the communication with Fiddler you have to, as mentioned in the last line above, add a dot after ‘localhost’ in your URL (if you are running on your local computer, otherwise never mind). Open Fiddler2 and monitor the communication and you’ll see something like this:

Communication monitoring in Fiddler
Communication caught in Fiddler, header highlighted in red

Read more in the next article: JIRA: Basic C#/JIRA fetching and displaying projects

Using jQuery in Jira

If you want to make some customizations for your JIRA install using javascript then there are some good news; jQuery is bundled into the package. The only small downside is that you’ll have to bind the $ alias yourself:

(function($){
   $(document).ready(function(){
      //your code here
   });  
})(AJS.$);

The AJS, Atlassian JavaScript, is a library that you can use when making gadgets in JIRA, but you can also use if if you’re just adding features in your banner. The documentation can be found here.

JIRA: Changing the URL to Confluence

If you need to change an application link in JIRA to, say Confluence, then it’s not completely obvious how to do this. First you go to Administration => Plugins => Application Links. From there you see your links (look for Confluence in the Application column). If you choose Configure on your link, however, you can configure both the Application Name and the Display URL, but not the Application URL. This is quite annoying and I had a bit of a hard time figuring out how to solve this. Luckily I found this guide which sort of explained how to do it.

The trick is to take Confluence offline and reload the Configure Application Link area. Then you’ll see a warning sign that says “Application Confluence seems to be offline. Click here to Relocate”. Click the “Click here to Relocate” sign and enter the new URL (and confirm that you want to save it EVEN if it is offline).
Tadaa, how obvious…