Invariant Properties

  • rss
  • Home

Using the JIRA REST java client: Comments and Attachments

Bear Giles | August 19, 2013

In 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.

  1. @Test
  2. public void createMinimalIssueWithComments() throws IOException, JSONException, InterruptedException {
  3.     final JiraRestClient restClient = new AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(
  4.             serverUri, username, password);
  5.  
  6.     final IssueInputBuilder builder = new IssueInputBuilder(PROJECT_KEY, BUG_TYPE_ID, SUMMARY);
  7.     final IssueInput input = builder.build();
  8.  
  9.     try {
  10.         // create issue
  11.         final IssueRestClient client = restClient.getIssueClient();
  12.         final BasicIssue issue = client.createIssue(input).claim();
  13.  
  14.         assertNotNull(issue.getKey());
  15.  
  16.         // create comments. Could also specify role or group visibility.
  17.         final Issue newIssue = client.getIssue(issue.getKey()).claim();
  18.  
  19.         final String contents1 = "contents 1";
  20.         client.addComment(newIssue.getCommentsUri(), Comment.valueOf(contents1)).claim();
  21.  
  22.         final String contents2 = "contents 2";
  23.         client.addComment(newIssue.getCommentsUri(), Comment.valueOf(contents2)).claim();
  24.  
  25.         // retrieve issue with comments
  26.         final Issue actual = client.getIssue(issue.getKey()).claim();
  27.         final User user = restClient.getUserClient().getUser(bundle.getString("username")).claim();
  28.  
  29.         // verify comments.
  30.         final Iterator<Comment> iterator = actual.getComments().iterator();
  31.         final Comment comment1 = iterator.next();
  32.         assertEquals(contents1, comment1.getBody());
  33.         assertEquals(user.getSelf(), comment1.getAuthor().getSelf());
  34.         assertNotNull(comment1.getCreationDate());
  35.  
  36.         final Comment comment2 = iterator.next();
  37.         assertEquals(contents2, comment2.getBody());
  38.         assertEquals(user.getSelf(), comment2.getAuthor().getSelf());
  39.         assertNotNull(comment2.getCreationDate());
  40.  
  41.         // post 2.0.0-m25 we can delete issue to reduce clutter
  42.         client.deleteIssue(issue.getKey(), false).claim();
  43.     } finally {
  44.         if (restClient != null) {
  45.             restClient.close();
  46.         }
  47.     }
  48. }
@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!

  1. /**
  2.  * Create an issue with two attachments. This test demonstrates how
  3.  * the MIMETYPE is extracted from the filename extension.
  4.  */
  5. @Test
  6. public void createMinimalIssueWithAttachments() throws IOException, JSONException, InterruptedException {
  7.     final JiraRestClient restClient = new AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(
  8.             serverUri, username, password);
  9.  
  10.     final IssueInputBuilder builder = new IssueInputBuilder(PROJECT_KEY, BUG_TYPE_ID, SUMMARY);
  11.     final IssueInput input = builder.build();
  12.  
  13.     try {
  14.         // create issue
  15.         final IssueRestClient client = restClient.getIssueClient();
  16.         final BasicIssue issue = client.createIssue(input).claim();
  17.  
  18.         assertNotNull(issue.getKey());
  19.  
  20.         // create attachments. JRJC does not provide way to set mime type.
  21.         final Issue newIssue = client.getIssue(issue.getKey()).claim();
  22.  
  23.         final String filename1 = "attachment1.txt";
  24.         final String contents1 = "contents 1";
  25.         client.addAttachment(newIssue.getAttachmentsUri(), new ByteArrayInputStream(contents1.getBytes()),
  26.                 filename1).claim();
  27.  
  28.         final String filename2 = "attachment2.html";
  29.         final String contents2 = "<html><body>contents 2</body></html>";
  30.         client.addAttachments(newIssue.getAttachmentsUri(),
  31.                 new AttachmentInput(filename2, new ByteArrayInputStream(contents2.getBytes()))).claim();
  32.  
  33.         // retrieve issue with attachments
  34.         final Issue actual = client.getIssue(issue.getKey()).claim();
  35.         final User user = restClient.getUserClient().getUser(bundle.getString("username")).claim();
  36.  
  37.         // verify attachments.
  38.         final Iterator<Attachment> iterator = actual.getAttachments().iterator();
  39.         final Attachment attachment1 = iterator.next();
  40.         assertEquals(filename1, attachment1.getFilename());
  41.         assertEquals(contents1.length(), attachment1.getSize());
  42.         assertEquals(user.getSelf(), attachment1.getAuthor().getSelf());
  43.         assertNotNull(attachment1.getCreationDate());
  44.         assertEquals("text/plain", attachment1.getMimeType());
  45.  
  46.         final Scanner scanner1 = new Scanner(client.getAttachment(attachment1.getContentUri()).claim());
  47.         scanner1.useDelimiter("^Z");
  48.         assertEquals(contents1, scanner1.next());
  49.         scanner1.close();
  50.  
  51.         final Attachment attachment2 = iterator.next();
  52.         assertEquals(filename2, attachment2.getFilename());
  53.         assertEquals(contents2.length(), attachment2.getSize());
  54.         assertEquals(user.getSelf(), attachment2.getAuthor().getSelf());
  55.         assertNotNull(attachment2.getCreationDate());
  56.         assertEquals("text/html", attachment2.getMimeType());
  57.  
  58.         final Scanner scanner2 = new Scanner(client.getAttachment(attachment2.getContentUri()).claim());
  59.         scanner2.useDelimiter("^Z");
  60.         assertEquals(contents2, scanner2.next());
  61.         scanner2.close();
  62.  
  63.         // post 2.0.0-m25 we can delete issue to reduce clutter
  64.         client.deleteIssue(issue.getKey(), false).claim();
  65.     } finally {
  66.         if (restClient != null) {
  67.             restClient.close();
  68.         }
  69.     }
  70. }
  71.  
  72. /**
  73.  * Create an issue with two attachments with the same filename.
  74.  * Takeaway: issues can have attachments with same filename - the
  75.  * second does NOT overwrite the first. This does flip the order
  76.  * of attachments seen above.
  77.  */
  78. @Test
  79. public void createMinimalIssueWithDuplicateAttachments() throws IOException, JSONException, InterruptedException {
  80.     final JiraRestClient restClient = new AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(
  81.             serverUri, username, password);
  82.  
  83.     final IssueInputBuilder builder = new IssueInputBuilder(PROJECT_KEY, BUG_TYPE_ID, SUMMARY);
  84.     final IssueInput input = builder.build();
  85.  
  86.     try {
  87.         // create issue
  88.         final IssueRestClient client = restClient.getIssueClient();
  89.         final BasicIssue issue = client.createIssue(input).claim();
  90.  
  91.         assertNotNull(issue.getKey());
  92.  
  93.         // create attachments. JRJC does not provide way to set mime type.
  94.         final Issue newIssue = client.getIssue(issue.getKey()).claim();
  95.  
  96.         final String filename1 = "attachment1.txt";
  97.         final String contents1 = "contents 1";
  98.         client.addAttachment(newIssue.getAttachmentsUri(), new ByteArrayInputStream(contents1.getBytes()),
  99.                 filename1).claim();
  100.  
  101.         final String filename2 = "attachment1.txt";
  102.         final String contents2 = "edited contents";
  103.         client.addAttachments(newIssue.getAttachmentsUri(),
  104.                 new AttachmentInput(filename2, new ByteArrayInputStream(contents2.getBytes()))).claim();
  105.  
  106.         // retrieve issue with attachments
  107.         final Issue actual = client.getIssue(issue.getKey()).claim();
  108.         final User user = restClient.getUserClient().getUser(bundle.getString("username")).claim();
  109.  
  110.         // verify attachments.
  111.         final Iterator<Attachment> iterator = actual.getAttachments().iterator();
  112.         final Attachment attachment1 = iterator.next();
  113.         assertEquals(filename1, attachment1.getFilename());
  114.         assertEquals(contents2.length(), attachment1.getSize());
  115.         assertEquals(user.getSelf(), attachment1.getAuthor().getSelf());
  116.         assertNotNull(attachment1.getCreationDate());
  117.         assertEquals("text/plain", attachment1.getMimeType());
  118.  
  119.         final Scanner scanner1 = new Scanner(client.getAttachment(attachment1.getContentUri()).claim());
  120.         scanner1.useDelimiter("^Z");
  121.         assertEquals(contents2, scanner1.next());
  122.         scanner1.close();
  123.  
  124.         final Attachment attachment2 = iterator.next();
  125.         assertEquals(filename1, attachment2.getFilename());
  126.         assertEquals(contents1.length(), attachment2.getSize());
  127.         assertEquals(user.getSelf(), attachment2.getAuthor().getSelf());
  128.         assertNotNull(attachment2.getCreationDate());
  129.         assertEquals("text/plain", attachment2.getMimeType());
  130.  
  131.         final Scanner scanner2 = new Scanner(client.getAttachment(attachment2.getContentUri()).claim());
  132.         scanner2.useDelimiter("^Z");
  133.         assertEquals(contents1, scanner2.next());
  134.         scanner2.close();
  135.  
  136.         assertFalse(iterator.hasNext());
  137.  
  138.         // post 2.0.0-m25 we can delete issue to reduce clutter
  139.         client.deleteIssue(issue.getKey(), false).claim();
  140.     } finally {
  141.         if (restClient != null) {
  142.             restClient.close();
  143.         }
  144.     }
  145. }
/**
 * 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.

Categories
java
Comments rss
Comments rss
Trackback
Trackback

« Estimating VO2max in Boulder Creating Password-Based Encryption Keys »

Leave a Reply

Click here to cancel reply.

You must be logged in to post a comment.

Archives

  • May 2020 (1)
  • March 2019 (1)
  • August 2018 (1)
  • May 2018 (1)
  • February 2018 (1)
  • November 2017 (4)
  • January 2017 (3)
  • June 2016 (1)
  • May 2016 (1)
  • April 2016 (2)
  • March 2016 (1)
  • February 2016 (3)
  • January 2016 (6)
  • December 2015 (2)
  • November 2015 (3)
  • October 2015 (2)
  • August 2015 (4)
  • July 2015 (2)
  • June 2015 (2)
  • January 2015 (1)
  • December 2014 (6)
  • October 2014 (1)
  • September 2014 (2)
  • August 2014 (1)
  • July 2014 (1)
  • June 2014 (2)
  • May 2014 (2)
  • April 2014 (1)
  • March 2014 (1)
  • February 2014 (3)
  • January 2014 (6)
  • December 2013 (13)
  • November 2013 (6)
  • October 2013 (3)
  • September 2013 (2)
  • August 2013 (5)
  • June 2013 (1)
  • May 2013 (2)
  • March 2013 (1)
  • November 2012 (1)
  • October 2012 (3)
  • September 2012 (2)
  • May 2012 (6)
  • January 2012 (2)
  • December 2011 (12)
  • July 2011 (1)
  • June 2011 (2)
  • May 2011 (5)
  • April 2011 (6)
  • March 2011 (4)
  • February 2011 (3)
  • October 2010 (6)
  • September 2010 (8)

Recent Posts

  • 8-bit Breadboard Computer: Good Encapsulation!
  • Where are all the posts?
  • Better Ad Blocking Through Pi-Hole and Local Caching
  • The difference between APIs and SPIs
  • Hadoop: User Impersonation with Kerberos Authentication

Meta

  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org

Pages

  • About Me
  • Notebook: Common XML Tasks
  • Notebook: Database/Webapp Security
  • Notebook: Development Tips

Syndication

Java Code Geeks

Know Your Rights

Support Bloggers' Rights
Demand Your dotRIGHTS

Security

  • Dark Reading
  • Krebs On Security Krebs On Security
  • Naked Security Naked Security
  • Schneier on Security Schneier on Security
  • TaoSecurity TaoSecurity

Politics

  • ACLU ACLU
  • EFF EFF

News

  • Ars technica Ars technica
  • Kevin Drum at Mother Jones Kevin Drum at Mother Jones
  • Raw Story Raw Story
  • Tech Dirt Tech Dirt
  • Vice Vice

Spam Blocked

53,793 spam blocked by Akismet
rss Comments rss valid xhtml 1.1 design by jide powered by Wordpress get firefox