Using the JIRA REST java client: Comments and Attachments
Bear Giles | August 19, 2013In the last article I discussed the JIRA issues – how to create simple issues. In this article I’ll discuss how to add comments and attachments.
Adding Comments
Issues can have an arbitrary number of searchable comments. Comments can be added via JRJC but can only be edited or deleted with the REST API. (/rest/api/2/issue/{issueIdOrKey}/comment/{id}, PUT and DELETE)
The only thing that the client can control are the comment’s body and visibility. The JIRA server will maintain the author and timestamp, for both the original comment and the latest update. The body is plain text, not HTML.
IMPORTANT: search results may only retrieve the initial comments! You must explicitly reload the issue to retrieve all comments.
- @Test
- public void createMinimalIssueWithComments() throws IOException, JSONException, InterruptedException {
- final JiraRestClient restClient = new AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(
- serverUri, username, password);
- final IssueInputBuilder builder = new IssueInputBuilder(PROJECT_KEY, BUG_TYPE_ID, SUMMARY);
- final IssueInput input = builder.build();
- try {
- // create issue
- final IssueRestClient client = restClient.getIssueClient();
- final BasicIssue issue = client.createIssue(input).claim();
- assertNotNull(issue.getKey());
- // create comments. Could also specify role or group visibility.
- final Issue newIssue = client.getIssue(issue.getKey()).claim();
- final String contents1 = "contents 1";
- client.addComment(newIssue.getCommentsUri(), Comment.valueOf(contents1)).claim();
- final String contents2 = "contents 2";
- client.addComment(newIssue.getCommentsUri(), Comment.valueOf(contents2)).claim();
- // retrieve issue with comments
- final Issue actual = client.getIssue(issue.getKey()).claim();
- final User user = restClient.getUserClient().getUser(bundle.getString("username")).claim();
- // verify comments.
- final Iterator<Comment> iterator = actual.getComments().iterator();
- final Comment comment1 = iterator.next();
- assertEquals(contents1, comment1.getBody());
- assertEquals(user.getSelf(), comment1.getAuthor().getSelf());
- assertNotNull(comment1.getCreationDate());
- final Comment comment2 = iterator.next();
- assertEquals(contents2, comment2.getBody());
- assertEquals(user.getSelf(), comment2.getAuthor().getSelf());
- assertNotNull(comment2.getCreationDate());
- // post 2.0.0-m25 we can delete issue to reduce clutter
- client.deleteIssue(issue.getKey(), false).claim();
- } finally {
- if (restClient != null) {
- restClient.close();
- }
- }
- }
@Test public void createMinimalIssueWithComments() throws IOException, JSONException, InterruptedException { final JiraRestClient restClient = new AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication( serverUri, username, password); final IssueInputBuilder builder = new IssueInputBuilder(PROJECT_KEY, BUG_TYPE_ID, SUMMARY); final IssueInput input = builder.build(); try { // create issue final IssueRestClient client = restClient.getIssueClient(); final BasicIssue issue = client.createIssue(input).claim(); assertNotNull(issue.getKey()); // create comments. Could also specify role or group visibility. final Issue newIssue = client.getIssue(issue.getKey()).claim(); final String contents1 = "contents 1"; client.addComment(newIssue.getCommentsUri(), Comment.valueOf(contents1)).claim(); final String contents2 = "contents 2"; client.addComment(newIssue.getCommentsUri(), Comment.valueOf(contents2)).claim(); // retrieve issue with comments final Issue actual = client.getIssue(issue.getKey()).claim(); final User user = restClient.getUserClient().getUser(bundle.getString("username")).claim(); // verify comments. final Iterator<Comment> iterator = actual.getComments().iterator(); final Comment comment1 = iterator.next(); assertEquals(contents1, comment1.getBody()); assertEquals(user.getSelf(), comment1.getAuthor().getSelf()); assertNotNull(comment1.getCreationDate()); final Comment comment2 = iterator.next(); assertEquals(contents2, comment2.getBody()); assertEquals(user.getSelf(), comment2.getAuthor().getSelf()); assertNotNull(comment2.getCreationDate()); // post 2.0.0-m25 we can delete issue to reduce clutter client.deleteIssue(issue.getKey(), false).claim(); } finally { if (restClient != null) { restClient.close(); } } }
Adding Attachments
Issues can also have an arbitrary number of attachments. IIRC attachments cannot be searched since they may contain binary data. Attachments are uploaded as multipart/form-data (RFC 1867) so they can have a declared MIMETYPE although it’s not configurable when using the JRJC library. (It guesses based on the filename extension but see below.)
As far as I can tell it is not possible to modify or delete attachments. A single issue can have multiple attachments with the same filename.
IMPORTANT: again search results may only retrieve the initial attachments!
- /**
- * Create an issue with two attachments. This test demonstrates how
- * the MIMETYPE is extracted from the filename extension.
- */
- @Test
- public void createMinimalIssueWithAttachments() throws IOException, JSONException, InterruptedException {
- final JiraRestClient restClient = new AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(
- serverUri, username, password);
- final IssueInputBuilder builder = new IssueInputBuilder(PROJECT_KEY, BUG_TYPE_ID, SUMMARY);
- final IssueInput input = builder.build();
- try {
- // create issue
- final IssueRestClient client = restClient.getIssueClient();
- final BasicIssue issue = client.createIssue(input).claim();
- assertNotNull(issue.getKey());
- // create attachments. JRJC does not provide way to set mime type.
- final Issue newIssue = client.getIssue(issue.getKey()).claim();
- final String filename1 = "attachment1.txt";
- final String contents1 = "contents 1";
- client.addAttachment(newIssue.getAttachmentsUri(), new ByteArrayInputStream(contents1.getBytes()),
- filename1).claim();
- final String filename2 = "attachment2.html";
- final String contents2 = "<html><body>contents 2</body></html>";
- client.addAttachments(newIssue.getAttachmentsUri(),
- new AttachmentInput(filename2, new ByteArrayInputStream(contents2.getBytes()))).claim();
- // retrieve issue with attachments
- final Issue actual = client.getIssue(issue.getKey()).claim();
- final User user = restClient.getUserClient().getUser(bundle.getString("username")).claim();
- // verify attachments.
- final Iterator<Attachment> iterator = actual.getAttachments().iterator();
- final Attachment attachment1 = iterator.next();
- assertEquals(filename1, attachment1.getFilename());
- assertEquals(contents1.length(), attachment1.getSize());
- assertEquals(user.getSelf(), attachment1.getAuthor().getSelf());
- assertNotNull(attachment1.getCreationDate());
- assertEquals("text/plain", attachment1.getMimeType());
- final Scanner scanner1 = new Scanner(client.getAttachment(attachment1.getContentUri()).claim());
- scanner1.useDelimiter("^Z");
- assertEquals(contents1, scanner1.next());
- scanner1.close();
- final Attachment attachment2 = iterator.next();
- assertEquals(filename2, attachment2.getFilename());
- assertEquals(contents2.length(), attachment2.getSize());
- assertEquals(user.getSelf(), attachment2.getAuthor().getSelf());
- assertNotNull(attachment2.getCreationDate());
- assertEquals("text/html", attachment2.getMimeType());
- final Scanner scanner2 = new Scanner(client.getAttachment(attachment2.getContentUri()).claim());
- scanner2.useDelimiter("^Z");
- assertEquals(contents2, scanner2.next());
- scanner2.close();
- // post 2.0.0-m25 we can delete issue to reduce clutter
- client.deleteIssue(issue.getKey(), false).claim();
- } finally {
- if (restClient != null) {
- restClient.close();
- }
- }
- }
- /**
- * Create an issue with two attachments with the same filename.
- * Takeaway: issues can have attachments with same filename - the
- * second does NOT overwrite the first. This does flip the order
- * of attachments seen above.
- */
- @Test
- public void createMinimalIssueWithDuplicateAttachments() throws IOException, JSONException, InterruptedException {
- final JiraRestClient restClient = new AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(
- serverUri, username, password);
- final IssueInputBuilder builder = new IssueInputBuilder(PROJECT_KEY, BUG_TYPE_ID, SUMMARY);
- final IssueInput input = builder.build();
- try {
- // create issue
- final IssueRestClient client = restClient.getIssueClient();
- final BasicIssue issue = client.createIssue(input).claim();
- assertNotNull(issue.getKey());
- // create attachments. JRJC does not provide way to set mime type.
- final Issue newIssue = client.getIssue(issue.getKey()).claim();
- final String filename1 = "attachment1.txt";
- final String contents1 = "contents 1";
- client.addAttachment(newIssue.getAttachmentsUri(), new ByteArrayInputStream(contents1.getBytes()),
- filename1).claim();
- final String filename2 = "attachment1.txt";
- final String contents2 = "edited contents";
- client.addAttachments(newIssue.getAttachmentsUri(),
- new AttachmentInput(filename2, new ByteArrayInputStream(contents2.getBytes()))).claim();
- // retrieve issue with attachments
- final Issue actual = client.getIssue(issue.getKey()).claim();
- final User user = restClient.getUserClient().getUser(bundle.getString("username")).claim();
- // verify attachments.
- final Iterator<Attachment> iterator = actual.getAttachments().iterator();
- final Attachment attachment1 = iterator.next();
- assertEquals(filename1, attachment1.getFilename());
- assertEquals(contents2.length(), attachment1.getSize());
- assertEquals(user.getSelf(), attachment1.getAuthor().getSelf());
- assertNotNull(attachment1.getCreationDate());
- assertEquals("text/plain", attachment1.getMimeType());
- final Scanner scanner1 = new Scanner(client.getAttachment(attachment1.getContentUri()).claim());
- scanner1.useDelimiter("^Z");
- assertEquals(contents2, scanner1.next());
- scanner1.close();
- final Attachment attachment2 = iterator.next();
- assertEquals(filename1, attachment2.getFilename());
- assertEquals(contents1.length(), attachment2.getSize());
- assertEquals(user.getSelf(), attachment2.getAuthor().getSelf());
- assertNotNull(attachment2.getCreationDate());
- assertEquals("text/plain", attachment2.getMimeType());
- final Scanner scanner2 = new Scanner(client.getAttachment(attachment2.getContentUri()).claim());
- scanner2.useDelimiter("^Z");
- assertEquals(contents1, scanner2.next());
- scanner2.close();
- assertFalse(iterator.hasNext());
- // post 2.0.0-m25 we can delete issue to reduce clutter
- client.deleteIssue(issue.getKey(), false).claim();
- } finally {
- if (restClient != null) {
- restClient.close();
- }
- }
- }
/** * Create an issue with two attachments. This test demonstrates how * the MIMETYPE is extracted from the filename extension. */ @Test public void createMinimalIssueWithAttachments() throws IOException, JSONException, InterruptedException { final JiraRestClient restClient = new AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication( serverUri, username, password); final IssueInputBuilder builder = new IssueInputBuilder(PROJECT_KEY, BUG_TYPE_ID, SUMMARY); final IssueInput input = builder.build(); try { // create issue final IssueRestClient client = restClient.getIssueClient(); final BasicIssue issue = client.createIssue(input).claim(); assertNotNull(issue.getKey()); // create attachments. JRJC does not provide way to set mime type. final Issue newIssue = client.getIssue(issue.getKey()).claim(); final String filename1 = "attachment1.txt"; final String contents1 = "contents 1"; client.addAttachment(newIssue.getAttachmentsUri(), new ByteArrayInputStream(contents1.getBytes()), filename1).claim(); final String filename2 = "attachment2.html"; final String contents2 = "<html><body>contents 2</body></html>"; client.addAttachments(newIssue.getAttachmentsUri(), new AttachmentInput(filename2, new ByteArrayInputStream(contents2.getBytes()))).claim(); // retrieve issue with attachments final Issue actual = client.getIssue(issue.getKey()).claim(); final User user = restClient.getUserClient().getUser(bundle.getString("username")).claim(); // verify attachments. final Iterator<Attachment> iterator = actual.getAttachments().iterator(); final Attachment attachment1 = iterator.next(); assertEquals(filename1, attachment1.getFilename()); assertEquals(contents1.length(), attachment1.getSize()); assertEquals(user.getSelf(), attachment1.getAuthor().getSelf()); assertNotNull(attachment1.getCreationDate()); assertEquals("text/plain", attachment1.getMimeType()); final Scanner scanner1 = new Scanner(client.getAttachment(attachment1.getContentUri()).claim()); scanner1.useDelimiter("^Z"); assertEquals(contents1, scanner1.next()); scanner1.close(); final Attachment attachment2 = iterator.next(); assertEquals(filename2, attachment2.getFilename()); assertEquals(contents2.length(), attachment2.getSize()); assertEquals(user.getSelf(), attachment2.getAuthor().getSelf()); assertNotNull(attachment2.getCreationDate()); assertEquals("text/html", attachment2.getMimeType()); final Scanner scanner2 = new Scanner(client.getAttachment(attachment2.getContentUri()).claim()); scanner2.useDelimiter("^Z"); assertEquals(contents2, scanner2.next()); scanner2.close(); // post 2.0.0-m25 we can delete issue to reduce clutter client.deleteIssue(issue.getKey(), false).claim(); } finally { if (restClient != null) { restClient.close(); } } } /** * Create an issue with two attachments with the same filename. * Takeaway: issues can have attachments with same filename - the * second does NOT overwrite the first. This does flip the order * of attachments seen above. */ @Test public void createMinimalIssueWithDuplicateAttachments() throws IOException, JSONException, InterruptedException { final JiraRestClient restClient = new AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication( serverUri, username, password); final IssueInputBuilder builder = new IssueInputBuilder(PROJECT_KEY, BUG_TYPE_ID, SUMMARY); final IssueInput input = builder.build(); try { // create issue final IssueRestClient client = restClient.getIssueClient(); final BasicIssue issue = client.createIssue(input).claim(); assertNotNull(issue.getKey()); // create attachments. JRJC does not provide way to set mime type. final Issue newIssue = client.getIssue(issue.getKey()).claim(); final String filename1 = "attachment1.txt"; final String contents1 = "contents 1"; client.addAttachment(newIssue.getAttachmentsUri(), new ByteArrayInputStream(contents1.getBytes()), filename1).claim(); final String filename2 = "attachment1.txt"; final String contents2 = "edited contents"; client.addAttachments(newIssue.getAttachmentsUri(), new AttachmentInput(filename2, new ByteArrayInputStream(contents2.getBytes()))).claim(); // retrieve issue with attachments final Issue actual = client.getIssue(issue.getKey()).claim(); final User user = restClient.getUserClient().getUser(bundle.getString("username")).claim(); // verify attachments. final Iterator<Attachment> iterator = actual.getAttachments().iterator(); final Attachment attachment1 = iterator.next(); assertEquals(filename1, attachment1.getFilename()); assertEquals(contents2.length(), attachment1.getSize()); assertEquals(user.getSelf(), attachment1.getAuthor().getSelf()); assertNotNull(attachment1.getCreationDate()); assertEquals("text/plain", attachment1.getMimeType()); final Scanner scanner1 = new Scanner(client.getAttachment(attachment1.getContentUri()).claim()); scanner1.useDelimiter("^Z"); assertEquals(contents2, scanner1.next()); scanner1.close(); final Attachment attachment2 = iterator.next(); assertEquals(filename1, attachment2.getFilename()); assertEquals(contents1.length(), attachment2.getSize()); assertEquals(user.getSelf(), attachment2.getAuthor().getSelf()); assertNotNull(attachment2.getCreationDate()); assertEquals("text/plain", attachment2.getMimeType()); final Scanner scanner2 = new Scanner(client.getAttachment(attachment2.getContentUri()).claim()); scanner2.useDelimiter("^Z"); assertEquals(contents1, scanner2.next()); scanner2.close(); assertFalse(iterator.hasNext()); // post 2.0.0-m25 we can delete issue to reduce clutter client.deleteIssue(issue.getKey(), false).claim(); } finally { if (restClient != null) { restClient.close(); } } }
IMPORTANT SECURITY NOTE: filename extensions and MIMETYPEs should never be trusted – many sites have been compromised because the security filters trusted images but the actual rendering code recognized and executed the payload as javascript. You should always be cautious when working with attachments.
IMPORTANT SECURITY NOTE, PART 2: You can’t trust images. Malicious images can cause buffer overflows.