Introduction
This tip presents another approach to create an issue in Atlassian Jira. Jira is an Issue and Project tracking software following Agile. The code and technique described in this tip can be used to programmatically connect to Jira and create an issue. It can be implemented inside an error handler module to automatically add issues to a Jira account when the program encounters an error. The error details are sent to Jira as Json and the inputs go into the respective fields under that project. This article also deals with sending an attachment along with the issue. Jira Rest API currently doesn't provide any way of sending the issue and the attachments together. What we need to do is that we have to send the issue first and parse the Json response to get the issue key. We can then use the issue key to send attachments to that issue.
Background
A basic idea about consuming a web service should be enough to understand the code presented in this tip. Since this is a console application, some idea about that is also needed. This tip is an alternative to the previous one. So the obvious question is that why doesn't this one use MSXML like the previous one. The reason is here. As explained in my previous article, obviously it works, but it is not recommended by Microsoft.
Using the Code
As you can see, the Jira class is the one that contains all the code. I will just browse through the rest as it is pretty straight-forward. In the class below, you just have to set the properties needed. The resource URL, the json string that contains the issue data, your jira credentials and the attachment filepaths.
Program.cs
namespace TryJira
{
class Program
{
static void Main(string[] args)
{
Jira objJira = new Jira();
objJira.Url = "YOUR_JIRA_URL";
objJira.JsonString = @"{""fields"" : {
""project"" : {
""key"" : ""YOUR_JIRA_PROJECT_KEY""},
""summary"" : ""ISSUE_SUMMARY"",
""description"" : ""ISSUE_DESCRIPTION"",
""issuetype"" : {
""name"" : ""ISSUE_TYPE""}}}";
objJira.UserName="YOUR_JIRA_USERNAME";
objJira.Password="YOUR_JIRA_PASSWORD";
objJira.filePaths = new List<string>() { "FILEPATH1","FILEPATH2" };
objJira.AddJiraIssue();
}
}
}
Utility.cs
The class below contains one utility method as of now. The GetEncodedCredentials()
method returns the base64 encoded credentials.
namespace TryJira
{
class Utility
{
public static string GetEncodedCredentials(string UserName, string Password)
{
string mergedCredentials = String.Format("{0}:{1}", UserName, Password);
byte[] byteCredentials = Encoding.UTF8.GetBytes(mergedCredentials);
return Convert.ToBase64String(byteCredentials);
}
}
}
Jira.cs
This class contains three methods and the properties. Let's skip the properties and go the first method.
namespace TryJira
{
class Jira
{
public string UserName { get; set; }
public string Password { get; set; }
public string Url { get; set; }
public string JsonString { get; set; }
public IEnumerable<string> filePaths { get; set; }
public void AddJiraIssue()
This method adds the issue to the project. Once we open the connection via POST
, we set the necessary headers. The content-type
is set to json
because we will be sending the project parameters as a json string. Next we set the authorization header and the credentials for basic HTTP authentication. We then convert the json string to bytes and write it to the request stream.
public void AddJiraIssue()
{
string restUrl = String.Format("{0}rest/api/2/issue/", Url);
HttpWebResponse response = null;
HttpWebRequest request = WebRequest.Create(restUrl) as HttpWebRequest;
request.Method = "POST";
request.Accept = "application/json";
request.ContentType = "application/json";
request.Headers.Add("Authorization", "Basic " +
Utility.GetEncodedCredentials(UserName,Password));
byte[] data = Encoding.UTF8.GetBytes(JsonString);
using (var requestStream = request.GetRequestStream())
{
requestStream.Write(data, 0, data.Length);
requestStream.Close();
}
Once the request is complete, we display the response on the console. Now, the response is in json. It contains the issue key and the link to the issue. We need the issue key to send attachments against that issue. We use the JavaScriptSerializer
Class in System.Web.Script.Serialization
to dereserialize the json response. A reference to System.Web.Extensions
has to be added to the project solution for this. We use the deserialize method in the class to parse the json string and get a dictionary containing the objects. Like I said, we only need one, that is the issue key. Once we get the key, we call the AddAttachments()
method, sending the key and the list of attachments as parameters.
The Json response after sending the issue looks like this:
{"id":"10111","key":"TP-59","self":
"https://YOUR_JIRA_URL/rest/api/2/issue/10111"}
Like I said, we need only the key, which is "TP-59
" in this case because we will be sending attachments against that issue. The other two fields are ignored, for now. All other details for the issue can be found at the URL provided in the "self
" field. The issue data will be available in Json format.
using (response = request.GetResponse() as HttpWebResponse)
{
if (response.StatusCode != HttpStatusCode.OK)
{
var reader = new StreamReader(response.GetResponseStream());
string str = reader.ReadToEnd();
Console.WriteLine("The server returned '{0}'\n{1}", response.StatusCode, str);
var jss = new System.Web.Script.Serialization.JavaScriptSerializer();
var sData = jss.Deserialize<Dictionary<string, string>>(str);
string issueKey = sData["key"].ToString();
AddAttachments(issueKey, filePaths);
}
}
request.Abort();
}
public void AddAttachments(string issueKey,IEnumerable<string> filePaths)
Well, now that we have the issue key, we simply need the attachments. Here, we simply check if the files to be attached are available and send the list of filepaths to the PostFile()
method.
public void AddAttachments(string issueKey,IEnumerable<string> filePaths)
{
string restUrl = String.Format("{0}rest/api/2/issue/{1}/attachments", Url, issueKey);
var filesToUpload = new List<FileInfo>();
foreach (var filePath in filePaths)
{
if (!File.Exists(filePath))
{
Console.WriteLine("File '{0}' doesn't exist", filePath);
}
var file = new FileInfo(filePath);
filesToUpload.Add(file);
}
if (filesToUpload.Count <= 0)
{
Console.WriteLine("No file to Upload");
}
PostFile(restUrl,filesToUpload);
}
You should consider reading the Jira Rest API documentation for the next part. It clearly states that the Jira resource expects a multipart post. That is what the next method does. It handles the multipart posting. Now, to separate the parts, we must use a boundary which is not a part of the original data. Using a globally unique identifier seems to be the easiest way. Now we have to create a stream containing the data in the files to be sent. We create a memory stream object for that and use a stream writer object to write to the stream. The documentation clearly states that name of the multipart/form-data
parameter that contains attachments must be "file". So we set the content disposition and the content type. After writing the contents of one file to the stream, we repeat the procedure for all the other files. I have kept the content-type
to application/octet stream
so that we can attach any type of file. In my case, I had to send one image(.png) and one XML file.
private void PostFile(string restUrl,IEnumerable<FileInfo> filePaths)
{
HttpWebResponse response = null;
HttpWebRequest request = null;
String boundary = String.Format("----------{0:N}", Guid.NewGuid());
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
foreach (var filePath in filePaths)
{
var fs = new FileStream(filePath.FullName, FileMode.Open, FileAccess.Read);
var data = new byte[fs.Length];
fs.Read(data, 0, data.Length);
fs.Close();
writer.WriteLine("--{0}", boundary);
writer.WriteLine("Content-Disposition: form-data; name=\"file\"; filename=\"{0}\"", filePath.Name);
writer.WriteLine("Content-Type: application/octet-stream");
writer.WriteLine();
writer.Flush();
stream.Write(data, 0, data.Length);
writer.WriteLine();
}
We then insert the boundary, flush the writer and set the stream position to 0
. The next part is exactly similar like sending the issue, which has been described above. The only addition is the "X-Atlassian-Token
" header which has been set to "nocheck
". This is needed because the method has XSRF protection. Omitting this header will simply block the request.
writer.WriteLine("--" + boundary + "--");
writer.Flush();
stream.Seek(0, SeekOrigin.Begin);
request = WebRequest.Create(restUrl) as HttpWebRequest;
request.Method = "POST";
request.ContentType = string.Format("multipart/form-data; boundary={0}", boundary);
request.Accept = "application/json";
request.Headers.Add("Authorization", "Basic " + Utility.GetEncodedCredentials(UserName, Password));
request.Headers.Add("X-Atlassian-Token", "nocheck");
request.ContentLength = stream.Length;
using (Stream requestStream = request.GetRequestStream())
{
stream.WriteTo(requestStream);
requestStream.Close();
}
using (response = request.GetResponse() as HttpWebResponse)
{
if (response.StatusCode != HttpStatusCode.OK)
{
var reader = new StreamReader(response.GetResponseStream());
Console.WriteLine("The server returned '{0}'\n{1}", response.StatusCode, reader.ReadToEnd());
}
}
request.Abort();
}
}
}
Screen of the Jira Environment
This is how the issue looks like in the Jira project that I had created for testing purposes. Best of luck!