From a6a6f97241608397988a2e3024b062c7190e9c7f Mon Sep 17 00:00:00 2001 From: Michael Hartle Date: Tue, 5 Apr 2016 13:06:49 +0200 Subject: [PATCH 01/14] Added JiraClient constructor to allow passing in a custom HttpClient, e.g. configured for proxying --- .../java/net/rcarz/jiraclient/JiraClient.java | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/main/java/net/rcarz/jiraclient/JiraClient.java b/src/main/java/net/rcarz/jiraclient/JiraClient.java index 18edb4f..62f90f5 100644 --- a/src/main/java/net/rcarz/jiraclient/JiraClient.java +++ b/src/main/java/net/rcarz/jiraclient/JiraClient.java @@ -50,7 +50,7 @@ public class JiraClient { * @throws JiraException */ public JiraClient(String uri) throws JiraException { - this(uri, null); + this(null, uri, null); } /** @@ -61,12 +61,26 @@ public class JiraClient { * @throws JiraException */ public JiraClient(String uri, ICredentials creds) throws JiraException { - PoolingClientConnectionManager connManager = new PoolingClientConnectionManager(); - connManager.setDefaultMaxPerRoute(20); - connManager.setMaxTotal(40); - HttpClient httpclient = new DefaultHttpClient(connManager); + this(null, uri, creds); + } + + /** + * Creates an authenticated JIRA client with custom HttpClient. + * + * @param httpClient Custom HttpClient to be used + * @param uri Base URI of the JIRA server + * @param creds Credentials to authenticate with + * @throws JiraException + */ + public JiraClient(HttpClient httpClient, String uri, ICredentials creds) throws JiraException { + if (httpClient == null) { + PoolingClientConnectionManager connManager = new PoolingClientConnectionManager(); + connManager.setDefaultMaxPerRoute(20); + connManager.setMaxTotal(40); + httpClient = new DefaultHttpClient(connManager); + } - restclient = new RestClient(httpclient, creds, URI.create(uri)); + restclient = new RestClient(httpClient, creds, URI.create(uri)); if (creds != null) { username = creds.getLogonName(); From 1790e0ca310f2c539d620d846c546a4339f73ba2 Mon Sep 17 00:00:00 2001 From: Magnus Lundberg Date: Fri, 8 Apr 2016 11:23:13 +0200 Subject: [PATCH 02/14] Added project category and email to project --- src/main/java/net/rcarz/jiraclient/Field.java | 2 + .../java/net/rcarz/jiraclient/Project.java | 12 ++ .../net/rcarz/jiraclient/ProjectCategory.java | 97 +++++++++++++++ .../net/rcarz/jiraclient/ProjectTest.java | 60 ++++++++++ src/test/java/net/rcarz/jiraclient/Utils.java | 112 ++++++++++++++++++ 5 files changed, 283 insertions(+) create mode 100644 src/main/java/net/rcarz/jiraclient/ProjectCategory.java create mode 100644 src/test/java/net/rcarz/jiraclient/ProjectTest.java diff --git a/src/main/java/net/rcarz/jiraclient/Field.java b/src/main/java/net/rcarz/jiraclient/Field.java index 87c0bdc..ccdb6c6 100644 --- a/src/main/java/net/rcarz/jiraclient/Field.java +++ b/src/main/java/net/rcarz/jiraclient/Field.java @@ -357,6 +357,8 @@ public final class Field { result = (T)new Priority(restclient, (JSONObject)r); else if (type == Project.class) result = (T)new Project(restclient, (JSONObject)r); + else if (type == ProjectCategory.class) + result = (T)new ProjectCategory(restclient, (JSONObject)r); else if (type == RemoteLink.class) result = (T)new RemoteLink(restclient, (JSONObject)r); else if (type == Resolution.class) diff --git a/src/main/java/net/rcarz/jiraclient/Project.java b/src/main/java/net/rcarz/jiraclient/Project.java index f60f5cd..fb674f6 100644 --- a/src/main/java/net/rcarz/jiraclient/Project.java +++ b/src/main/java/net/rcarz/jiraclient/Project.java @@ -41,6 +41,8 @@ public class Project extends Resource { private List issueTypes = null; private List versions = null; private Map roles = null; + private ProjectCategory category = null; + private String email = null; /** * Creates a project from a JSON payload. @@ -73,6 +75,8 @@ public class Project extends Resource { restclient); versions = Field.getResourceArray(Version.class, map.get("versions"), restclient); roles = Field.getMap(String.class, String.class, map.get("roles")); + category = Field.getResource(ProjectCategory.class, map.get( "projectCategory" ), restclient); + email = Field.getString( map.get("email")); } /** @@ -170,5 +174,13 @@ public class Project extends Resource { public Map getRoles() { return roles; } + + public ProjectCategory getCategory() { + return category; + } + + public String getEmail() { + return email; + } } diff --git a/src/main/java/net/rcarz/jiraclient/ProjectCategory.java b/src/main/java/net/rcarz/jiraclient/ProjectCategory.java new file mode 100644 index 0000000..1c7c48a --- /dev/null +++ b/src/main/java/net/rcarz/jiraclient/ProjectCategory.java @@ -0,0 +1,97 @@ +/** + * jira-client - a simple JIRA REST client + * Copyright (c) 2013 Bob Carroll (bob.carroll@alum.rit.edu) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package net.rcarz.jiraclient; + +import java.util.Map; + +import net.sf.json.JSON; +import net.sf.json.JSONObject; + +/** + * Represents a project category. + */ +public class ProjectCategory extends Resource { + + private String name = null; + private String description = null; + + /** + * Creates a category from a JSON payload. + * + * @param restclient REST client instance + * @param json JSON payload + */ + protected ProjectCategory(RestClient restclient, JSONObject json) { + super(restclient); + + if (json != null) + deserialise(json); + } + + private void deserialise(JSONObject json) { + Map map = json; + + self = Field.getString(map.get("self")); + id = Field.getString(map.get("id")); + description = Field.getString(map.get("description")); + name = Field.getString(map.get("name")); + } + + /** + * Retrieves the given status record. + * + * @param restclient REST client instance + * @param id Internal JIRA ID of the status + * + * @return a status instance + * + * @throws JiraException when the retrieval fails + */ + public static ProjectCategory get(RestClient restclient, String id) + throws JiraException { + + JSON result = null; + + try { + result = restclient.get(getBaseUri() + "projectCategory/" + id); + } catch (Exception ex) { + throw new JiraException("Failed to retrieve status " + id, ex); + } + + if (!(result instanceof JSONObject)) + throw new JiraException("JSON payload is malformed"); + + return new ProjectCategory(restclient, (JSONObject)result); + } + + @Override + public String toString() { + return getName(); + } + + public String getDescription() { + return description; + } + + public String getName() { + return name; + } +} + diff --git a/src/test/java/net/rcarz/jiraclient/ProjectTest.java b/src/test/java/net/rcarz/jiraclient/ProjectTest.java new file mode 100644 index 0000000..8121f6e --- /dev/null +++ b/src/test/java/net/rcarz/jiraclient/ProjectTest.java @@ -0,0 +1,60 @@ +package net.rcarz.jiraclient; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertTrue; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; + +public class ProjectTest { + + @Test + public void testCreateProject() { + new Project(null, Utils.getTestProject()); + } + + @Test + public void projectDataTest() + { + String email = "from-jira@example.com"; + String assigneeType = "PROJECT_LEAD"; + String name = "Example"; + String self = "http://www.example.com/jira/rest/api/2/project/EX"; + String id = "10000"; + String key = "EX"; + String description = "This project was created as an example for REST."; + Project project = new Project(null, Utils.getTestProject()); + + assertEquals(description, project.getDescription()); + assertEquals(name, project.getName()); + assertEquals(id, project.getId()); + assertEquals(email, project.getEmail()); + assertEquals(assigneeType, project.getAssigneeType()); + assertTrue(project.getVersions().isEmpty()); + assertEquals(name, project.getName()); + assertEquals(self, project.getSelf()); + assertEquals( key, project.getKey()); + } + @Test + public void projectIssueTypesTest() + { + Project project = new Project(null, Utils.getTestProject()); + assertEquals(2, project.getIssueTypes().size()); + assertEquals("Task", project.getIssueTypes().get(0).getName()); + assertEquals("Bug", project.getIssueTypes().get(1).getName()); + } + @Test + public void projectCategoryTest() + { + String name = "FIRST"; + String id = "10000"; + String description = "First Project Category"; + + Project project = new Project(null, Utils.getTestProject()); + assertNotNull(project.getCategory()); + assertEquals(description, project.getCategory().getDescription()); + + assertEquals(name, project.getCategory().getName()); + assertEquals(id, project.getCategory().getId()); + } +} diff --git a/src/test/java/net/rcarz/jiraclient/Utils.java b/src/test/java/net/rcarz/jiraclient/Utils.java index b06ef1e..1ac16f9 100644 --- a/src/test/java/net/rcarz/jiraclient/Utils.java +++ b/src/test/java/net/rcarz/jiraclient/Utils.java @@ -281,4 +281,116 @@ public class Utils { return jsonObject; } + public static JSONObject getTestProject() { + JSONObject jsonObject = (JSONObject) JSONSerializer.toJSON("{" + + " \"expand\": \"description,lead,url,projectKeys\"," + + " \"self\": \"http://www.example.com/jira/rest/api/2/project/EX\"," + + " \"id\": \"10000\"," + + " \"key\": \"EX\"," + + " \"description\": \"This project was created as an example for REST.\"," + + " \"lead\": {" + + " \"self\": \"http://www.example.com/jira/rest/api/2/user?username=fred\"," + + " \"name\": \"fred\"," + + " \"avatarUrls\": {" + + " \"48x48\": \"http://www.example.com/jira/secure/useravatar?size=large&ownerId=fred\"," + + " \"24x24\": \"http://www.example.com/jira/secure/useravatar?size=small&ownerId=fred\"," + + " \"16x16\": \"http://www.example.com/jira/secure/useravatar?size=xsmall&ownerId=fred\"," + + " \"32x32\": \"http://www.example.com/jira/secure/useravatar?size=medium&ownerId=fred\"" + + " }," + + " \"displayName\": \"Fred F. User\"," + + " \"active\": false" + + " }," + + " \"components\": [" + + " {" + + " \"self\": \"http://www.example.com/jira/rest/api/2/component/10000\"," + + " \"id\": \"10000\"," + + " \"name\": \"Component 1\"," + + " \"description\": \"This is a JIRA component\"," + + " \"lead\": {" + + " \"self\": \"http://www.example.com/jira/rest/api/2/user?username=fred\"," + + " \"name\": \"fred\"," + + " \"avatarUrls\": {" + + " \"48x48\": \"http://www.example.com/jira/secure/useravatar?size=large&ownerId=fred\"," + + " \"24x24\": \"http://www.example.com/jira/secure/useravatar?size=small&ownerId=fred\"," + + " \"16x16\": \"http://www.example.com/jira/secure/useravatar?size=xsmall&ownerId=fred\"," + + " \"32x32\": \"http://www.example.com/jira/secure/useravatar?size=medium&ownerId=fred\"" + + " }," + + " \"displayName\": \"Fred F. User\"," + + " \"active\": false" + + " }," + + " \"assigneeType\": \"PROJECT_LEAD\"," + + " \"assignee\": {" + + " \"self\": \"http://www.example.com/jira/rest/api/2/user?username=fred\"," + + " \"name\": \"fred\"," + + " \"avatarUrls\": {" + + " \"48x48\": \"http://www.example.com/jira/secure/useravatar?size=large&ownerId=fred\"," + + " \"24x24\": \"http://www.example.com/jira/secure/useravatar?size=small&ownerId=fred\"," + + " \"16x16\": \"http://www.example.com/jira/secure/useravatar?size=xsmall&ownerId=fred\"," + + " \"32x32\": \"http://www.example.com/jira/secure/useravatar?size=medium&ownerId=fred\"" + + " }," + + " \"displayName\": \"Fred F. User\"," + + " \"active\": false" + + " }," + + " \"realAssigneeType\": \"PROJECT_LEAD\"," + + " \"realAssignee\": {" + + " \"self\": \"http://www.example.com/jira/rest/api/2/user?username=fred\"," + + " \"name\": \"fred\"," + + " \"avatarUrls\": {" + + " \"48x48\": \"http://www.example.com/jira/secure/useravatar?size=large&ownerId=fred\"," + + " \"24x24\": \"http://www.example.com/jira/secure/useravatar?size=small&ownerId=fred\"," + + " \"16x16\": \"http://www.example.com/jira/secure/useravatar?size=xsmall&ownerId=fred\"," + + " \"32x32\": \"http://www.example.com/jira/secure/useravatar?size=medium&ownerId=fred\"" + + " }," + + " \"displayName\": \"Fred F. User\"," + + " \"active\": false" + + " }," + + " \"isAssigneeTypeValid\": false," + + " \"project\": \"HSP\"," + + " \"projectId\": 10000" + + " }" + + " ]," + + " \"issueTypes\": [" + + " {" + + " \"self\": \"http://localhost:8090/jira/rest/api/2.0/issueType/3\"," + + " \"id\": \"3\"," + + " \"description\": \"A task that needs to be done.\"," + + " \"iconUrl\": \"http://localhost:8090/jira/images/icons/issuetypes/task.png\"," + + " \"name\": \"Task\"," + + " \"subtask\": false," + + " \"avatarId\": 1" + + " }," + + " {" + + " \"self\": \"http://localhost:8090/jira/rest/api/2.0/issueType/1\"," + + " \"id\": \"1\"," + + " \"description\": \"A problem with the software.\"," + + " \"iconUrl\": \"http://localhost:8090/jira/images/icons/issuetypes/bug.png\"," + + " \"name\": \"Bug\"," + + " \"subtask\": false," + + " \"avatarId\": 10002" + + " }" + + " ]," + + " \"url\": \"http://www.example.com/jira/browse/EX\"," + + " \"email\": \"from-jira@example.com\"," + + " \"assigneeType\": \"PROJECT_LEAD\"," + + " \"versions\": []," + + " \"name\": \"Example\"," + + " \"roles\": {" + + " \"Developers\": \"http://www.example.com/jira/rest/api/2/project/EX/role/10000\"" + + " }," + + " \"avatarUrls\": {" + + " \"48x48\": \"http://www.example.com/jira/secure/projectavatar?size=large&pid=10000\"," + + " \"24x24\": \"http://www.example.com/jira/secure/projectavatar?size=small&pid=10000\"," + + " \"16x16\": \"http://www.example.com/jira/secure/projectavatar?size=xsmall&pid=10000\"," + + " \"32x32\": \"http://www.example.com/jira/secure/projectavatar?size=medium&pid=10000\"" + + " }," + + " \"projectCategory\": {" + + " \"self\": \"http://www.example.com/jira/rest/api/2/projectCategory/10000\"," + + " \"id\": \"10000\"," + + " \"name\": \"FIRST\"," + + " \"description\": \"First Project Category\"" + + " }" + + "}"); + return jsonObject; + } + } From d32efebbd76547e5b0f5fa05c473460af7dbb5f9 Mon Sep 17 00:00:00 2001 From: Magnus Lundberg Date: Fri, 8 Apr 2016 12:03:17 +0200 Subject: [PATCH 03/14] Added Magnus Lundberg to Authors --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index 7defe0b..3449339 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -3,3 +3,4 @@ Kyle Chaplin @chaplinkyle Alesandro Lang @alesandroLang Javier Molina @javinovich Joseph McCarthy +Magnus Lundberg \ No newline at end of file From 027a407109c041a41be8b14ed3d6a513e2073b8a Mon Sep 17 00:00:00 2001 From: "jay.guidos" Date: Wed, 25 May 2016 09:27:41 -0400 Subject: [PATCH 04/14] Add support for JIRA custom field type "option" --- src/main/java/net/rcarz/jiraclient/Field.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/rcarz/jiraclient/Field.java b/src/main/java/net/rcarz/jiraclient/Field.java index 87c0bdc..b89be72 100644 --- a/src/main/java/net/rcarz/jiraclient/Field.java +++ b/src/main/java/net/rcarz/jiraclient/Field.java @@ -550,9 +550,11 @@ public final class Field { itemMap.put(ValueType.NAME.toString(), realValue.toString()); realResult = itemMap; - } else if (type.equals("string") && custom != null + } else if ( type.equals("option") || + ( + type.equals("string") && custom != null && (custom.equals("com.atlassian.jira.plugin.system.customfieldtypes:multicheckboxes") || - custom.equals("com.atlassian.jira.plugin.system.customfieldtypes:multiselect"))) { + custom.equals("com.atlassian.jira.plugin.system.customfieldtypes:multiselect")))) { realResult = new JSONObject(); ((JSONObject)realResult).put(ValueType.VALUE.toString(), realValue.toString()); From 77ba00abeb30feac71b312f73c45c0a0e3e06bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Krein=C3=B8e?= Date: Fri, 27 May 2016 10:32:53 +0200 Subject: [PATCH 05/14] Made all the time fields in WorkLog include the time and not just the date. This fix #156 --- src/main/java/net/rcarz/jiraclient/WorkLog.java | 6 +++--- .../java/net/rcarz/jiraclient/WorklogTest.java | 17 ++++++----------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/main/java/net/rcarz/jiraclient/WorkLog.java b/src/main/java/net/rcarz/jiraclient/WorkLog.java index 4c4d3f2..50b48c9 100644 --- a/src/main/java/net/rcarz/jiraclient/WorkLog.java +++ b/src/main/java/net/rcarz/jiraclient/WorkLog.java @@ -60,10 +60,10 @@ public class WorkLog extends Resource { id = Field.getString(map.get("id")); author = Field.getResource(User.class, map.get("author"), restclient); comment = Field.getString(map.get("comment")); - created = Field.getDate(map.get("created")); - updated = Field.getDate(map.get("updated")); + created = Field.getDateTime(map.get("created")); + updated = Field.getDateTime(map.get("updated")); updateAuthor = Field.getResource(User.class, map.get("updateAuthor"), restclient); - started = Field.getDate(map.get("started")); + started = Field.getDateTime(map.get("started")); timeSpent = Field.getString(map.get("timeSpent")); timeSpentSeconds = Field.getInteger(map.get("timeSpentSeconds")); } diff --git a/src/test/java/net/rcarz/jiraclient/WorklogTest.java b/src/test/java/net/rcarz/jiraclient/WorklogTest.java index ab610ad..2d6e12f 100644 --- a/src/test/java/net/rcarz/jiraclient/WorklogTest.java +++ b/src/test/java/net/rcarz/jiraclient/WorklogTest.java @@ -43,12 +43,8 @@ public class WorklogTest { userJSON.put("name","Joseph McCarthy"); mockJSONObject.put("author", userJSON); - String DATE_FORMAT = "yyyy-MM-dd"; - SimpleDateFormat df = new SimpleDateFormat(DATE_FORMAT); - final Date parse = df.parse(dateString, new ParsePosition(0)); - WorkLog workLog = new WorkLog(mockRestClient,mockJSONObject); - assertEquals(parse.toString() + " by Joseph McCarthy",workLog.toString()); + assertEquals(workLog.getCreatedDate() + " by Joseph McCarthy",workLog.toString()); } @Test @@ -63,14 +59,13 @@ public class WorklogTest { assertEquals("45517", workLog.getId()); String author = "joseph"; assertEquals(author, workLog.getAuthor().getName()); - String started = "2015-08-17T00:00:00.000"; - assertEquals(started, simpleDateFormat.format(workLog.getStarted())); - String created = "2015-08-20T00:00:00.000"; - assertEquals(created, simpleDateFormat.format(workLog.getCreatedDate())); + final long expectedStartedUnixTimeStamp = 1439803140000L; //unix timestamp in millis of 2015-08-17T13:19:00.000+0400 + assertEquals(expectedStartedUnixTimeStamp, workLog.getStarted().getTime()); + final long expectedCreatedAndUpdatedUnitTimeStamp = 1440062384000L; //unix timestamp in millis of 2015-08-20T13:19:44.000+0400 + assertEquals(expectedCreatedAndUpdatedUnitTimeStamp, workLog.getCreatedDate().getTime()); + assertEquals(expectedCreatedAndUpdatedUnitTimeStamp, workLog.getUpdatedDate().getTime()); assertEquals(21600, workLog.getTimeSpentSeconds()); assertEquals(author, workLog.getUpdateAuthor().getName()); - assertEquals(created, simpleDateFormat.format(workLog.getUpdatedDate())); - } } From dda6a4ec7842fc0367332aff5e0cd9ad4c41dfd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Krein=C3=B8e?= Date: Mon, 30 May 2016 12:45:39 +0200 Subject: [PATCH 06/14] Made searches return an empty SearchResult instance instead of throwing an exception on no result. This fixes #158 --- src/main/java/net/rcarz/jiraclient/Issue.java | 2 +- .../java/net/rcarz/jiraclient/SearchTest.java | 55 ++++++++++++++++++- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/rcarz/jiraclient/Issue.java b/src/main/java/net/rcarz/jiraclient/Issue.java index eafb6e8..039a05d 100644 --- a/src/main/java/net/rcarz/jiraclient/Issue.java +++ b/src/main/java/net/rcarz/jiraclient/Issue.java @@ -595,7 +595,7 @@ public class Issue extends Resource { // first call if (currentPage == null) { currentPage = getNextIssues().iterator(); - if (currentPage == null) { + if (currentPage == null || !currentPage.hasNext()) { return null; } else { return currentPage.next(); diff --git a/src/test/java/net/rcarz/jiraclient/SearchTest.java b/src/test/java/net/rcarz/jiraclient/SearchTest.java index 0c0ccac..942841c 100644 --- a/src/test/java/net/rcarz/jiraclient/SearchTest.java +++ b/src/test/java/net/rcarz/jiraclient/SearchTest.java @@ -2,9 +2,13 @@ package net.rcarz.jiraclient; import org.junit.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import java.nio.channels.Pipe; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import static org.junit.Assert.*; public class SearchTest { @@ -21,6 +25,51 @@ public class SearchTest { assertEquals("and resolution Fixed", "Fixed", searchResult.issues.get(0).getResolution().getName()); } + @Test + public void testEmptySearchGivesEmptyResult() throws JiraException { + final JiraClient jira = new JiraClient("https://jira.atlassian.com/", null); + + //Valid jql query that will always yield no results. + final String q = "key = NotExisting-9999999 AND key = blah-8833772"; + Issue.SearchResult searchResult = jira.searchIssues(q); + + final String assertMsg = "Searches that yield no results, should return an empty " + + Issue.SearchResult.class.getSimpleName() + " instance"; + assertTrue(assertMsg, searchResult.issues.isEmpty()); + assertFalse(assertMsg, searchResult.issues.iterator().hasNext()); + assertEquals(assertMsg, 0, searchResult.total); + assertEquals(assertMsg, 0, searchResult.start); + } + + @Test + public void testSearchResultIteratorWithinMaxResultLimit() throws JiraException { + final JiraClient jira = new JiraClient("https://jira.atlassian.com/", null); + final int usedMax = 2; + //Will return everything from the public Jira for Jira + final Issue.SearchResult searchResult = jira.searchIssues("", usedMax); + final List iterResults = new ArrayList(usedMax); + final Iterator iterator = searchResult.iterator(); + for (int i = 0 ; i < usedMax ; i++) { + iterResults.add(iterator.next()); + } + assertEquals(searchResult.issues, iterResults); + } + + @Test + public void testIterateBeyondMaxResult() throws JiraException { + final JiraClient jira = new JiraClient("https://jira.atlassian.com/", null); + + //Will return everything from the public Jira for Jira (at the time of writing 163697 issues). + final int usedMax = 2; + Issue.SearchResult searchResult = jira.searchIssues("", usedMax); + final Iterator iterator = searchResult.iterator(); + System.out.println(searchResult.issues); + for (int i = 0 ; i < 3 ; i++) { + //Running this 3 times without failing, ensures the it can fetch issues beyond the first fetch batch size, as the used max result is only 2. + iterator.next(); + } + } + @Test public void testExpandingChangeLogInSearch() throws JiraException { JiraClient jira = new JiraClient("https://jira.atlassian.com/", null); From cadd67d17a943999ce260e972bf83ef9401e96b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Krein=C3=B8e?= Date: Mon, 30 May 2016 13:04:18 +0200 Subject: [PATCH 07/14] Added more detailed javadoc to Issue.SearchResult. This work is part of fixing #158 --- src/main/java/net/rcarz/jiraclient/Issue.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/net/rcarz/jiraclient/Issue.java b/src/main/java/net/rcarz/jiraclient/Issue.java index 039a05d..f26f591 100644 --- a/src/main/java/net/rcarz/jiraclient/Issue.java +++ b/src/main/java/net/rcarz/jiraclient/Issue.java @@ -658,6 +658,17 @@ public class Issue extends Resource { /** * Issue search results structure. + * + * The issues of the result can be retrived from this class in 2 ways. + * + * The first is to access the issues field directly. This is a list of Issue instances. + * Note however that this will only contain the issues fetched in the initial search, + * so its size will be the same as the max result value or below. + * + * The second way is to use the iterator methods. This will return an Iterator instance, + * that will iterate over every result of the search, even if there is more than the max + * result value. The price for this, is that the call to next has none determistic performence, + * as it sometimes need to fetch a new batch of issues from Jira. */ public static class SearchResult { public int start = 0; From 5af0411af5b9d55704204ab90f4e2b9c175ec742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Krein=C3=B8e?= Date: Mon, 30 May 2016 13:06:22 +0200 Subject: [PATCH 08/14] Made Issue.SearchResult.iterator() return an Iterator instead of IssueIterator, as this is a private inner class, and can be accessed by clients anyway. This work is part of fixing #158 --- src/main/java/net/rcarz/jiraclient/Issue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/rcarz/jiraclient/Issue.java b/src/main/java/net/rcarz/jiraclient/Issue.java index f26f591..00e4936 100644 --- a/src/main/java/net/rcarz/jiraclient/Issue.java +++ b/src/main/java/net/rcarz/jiraclient/Issue.java @@ -705,7 +705,7 @@ public class Issue extends Resource { * * @return All issues found. */ - public IssueIterator iterator() { + public Iterator iterator() { return issueIterator; } } From e099ff8111359abb76ab355f24468be7b846fd31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Krein=C3=B8e?= Date: Wed, 25 May 2016 09:25:34 +0200 Subject: [PATCH 09/14] Added support for fetching a Jira filter --- AUTHORS.md | 3 +- .../java/net/rcarz/jiraclient/Filter.java | 74 +++++++++++++++++++ .../java/net/rcarz/jiraclient/JiraClient.java | 10 +++ .../java/net/rcarz/jiraclient/FilterTest.java | 29 ++++++++ 4 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 src/main/java/net/rcarz/jiraclient/Filter.java create mode 100644 src/test/java/net/rcarz/jiraclient/FilterTest.java diff --git a/AUTHORS.md b/AUTHORS.md index 3449339..3b155c7 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -3,4 +3,5 @@ Kyle Chaplin @chaplinkyle Alesandro Lang @alesandroLang Javier Molina @javinovich Joseph McCarthy -Magnus Lundberg \ No newline at end of file +Magnus Lundberg +Anders Kreinøe @Kreinoee diff --git a/src/main/java/net/rcarz/jiraclient/Filter.java b/src/main/java/net/rcarz/jiraclient/Filter.java new file mode 100644 index 0000000..f2756f9 --- /dev/null +++ b/src/main/java/net/rcarz/jiraclient/Filter.java @@ -0,0 +1,74 @@ +package net.rcarz.jiraclient; + +import net.sf.json.JSON; +import net.sf.json.JSONObject; + +import java.net.URI; +import java.util.Map; + +/** + * Represens a Jira filter. + */ +public class Filter extends Resource { + + private String name; + private String jql; + private boolean favourite; + + public Filter(RestClient restclient, JSONObject json) { + super(restclient); + + if (json != null) + deserialise(json); + } + + private void deserialise(JSONObject json) { + Map map = json; + + id = Field.getString(map.get("id")); + self = Field.getString(map.get("self")); + name = Field.getString(map.get("name")); + jql = Field.getString(map.get("jql")); + favourite = Field.getBoolean(map.get("favourite")); + } + + public boolean isFavourite() { + return favourite; + } + + public String getJql() { + return jql; + } + + public String getName() { + return name; + } + + public static Filter get(final RestClient restclient, final String id) throws JiraException { + JSON result = null; + + try { + URI uri = restclient.buildURI(getBaseUri() + "filter/" + id); + result = restclient.get(uri); + } catch (Exception ex) { + throw new JiraException("Failed to retrieve filter with id " + id, ex); + } + + if (!(result instanceof JSONObject)) { + throw new JiraException("JSON payload is malformed"); + } + + return new Filter(restclient, (JSONObject) result); + } + + @Override + public String toString() { + return "Filter{" + + "favourite=" + favourite + + ", name='" + name + '\'' + + ", jql='" + jql + '\'' + + '}'; + } + + +} diff --git a/src/main/java/net/rcarz/jiraclient/JiraClient.java b/src/main/java/net/rcarz/jiraclient/JiraClient.java index 18edb4f..70dd1b7 100644 --- a/src/main/java/net/rcarz/jiraclient/JiraClient.java +++ b/src/main/java/net/rcarz/jiraclient/JiraClient.java @@ -354,6 +354,16 @@ public class JiraClient { startAt); } + /** + * Retrieve the jira filter with the supplied id. + * @param id id of the filter to retreive. + * @return The Jira filter with the supplied id + * @throws JiraException + */ + public Filter getFilter(final String id) throws JiraException { + return Filter.get(restclient, id); + } + /** * * @return a list of all priorities available in the Jira installation diff --git a/src/test/java/net/rcarz/jiraclient/FilterTest.java b/src/test/java/net/rcarz/jiraclient/FilterTest.java new file mode 100644 index 0000000..a54edfd --- /dev/null +++ b/src/test/java/net/rcarz/jiraclient/FilterTest.java @@ -0,0 +1,29 @@ +package net.rcarz.jiraclient; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * Test cases for stuff relating to filters. + */ +public class FilterTest { + + @Test + public void testGetFilter() throws JiraException { + JiraClient jira = new JiraClient("https://jira.atlassian.com/", null); + + String id = "12844"; + Filter filter = jira.getFilter(id); + + assertNotNull(filter); + assertEquals("with id " + id, id, filter.getId()); + final String expectedName = "All JIRA Bugs"; + assertEquals("with name " + expectedName, expectedName, filter.getName()); + final String expectedJql = "project = 10240 AND issuetype = 1 ORDER BY key DESC"; + assertEquals("with jql: " + expectedJql, expectedJql, filter.getJql()); + assertEquals("None favourite", false, filter.isFavourite()); + } + +} From e2781229db144b6d533e3ce09607d6f4798fd649 Mon Sep 17 00:00:00 2001 From: Andrey Kuzmin Date: Tue, 15 Mar 2016 22:18:06 +0300 Subject: [PATCH 10/14] Implemented delete issue. Added Mockito dependency for test. --- AUTHORS.md | 1 + pom.xml | 1 - src/main/java/net/rcarz/jiraclient/Issue.java | 26 ++++++++++++++----- .../java/net/rcarz/jiraclient/IssueTest.java | 16 ++++++++++++ 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 3b155c7..ffd2b46 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -5,3 +5,4 @@ Javier Molina @javinovich Joseph McCarthy Magnus Lundberg Anders Kreinøe @Kreinoee +Andrey Kuzmin @nach-o-man diff --git a/pom.xml b/pom.xml index 31d727c..a1b9203 100644 --- a/pom.xml +++ b/pom.xml @@ -85,6 +85,5 @@ test - diff --git a/src/main/java/net/rcarz/jiraclient/Issue.java b/src/main/java/net/rcarz/jiraclient/Issue.java index eafb6e8..45b4c8d 100644 --- a/src/main/java/net/rcarz/jiraclient/Issue.java +++ b/src/main/java/net/rcarz/jiraclient/Issue.java @@ -1,7 +1,7 @@ /** * jira-client - a simple JIRA REST client * Copyright (c) 2013 Bob Carroll (bob.carroll@alum.rit.edu) - * + * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either @@ -876,7 +876,7 @@ public class Issue extends Resource { restclient); if (projects.isEmpty() || projects.get(0).getIssueTypes().isEmpty()) - throw new JiraException("Project '"+ project + "' or issue type '" + issueType + + throw new JiraException("Project '"+ project + "' or issue type '" + issueType + "' missing from create metadata. Do you have enough permissions?"); return projects.get(0).getIssueTypes().get(0).getFields(); @@ -947,7 +947,7 @@ public class Issue extends Resource { throw new JiraException("Failed add attachment to issue " + key, ex); } } - + /** * Adds a remote link to this issue. * @@ -1248,7 +1248,7 @@ public class Issue extends Resource { *
  • summary,comment - include just the summary and comments
  • *
  • *all,-comment - include all fields
  • * - * + * * @param expand fields to expand when obtaining the issue * * @return an issue instance @@ -1329,10 +1329,10 @@ public class Issue extends Resource { *
  • summary,comment - include just the summary and comments
  • *
  • *all,-comment - include all fields
  • * - * + * * @param maxResults if non-null, defines the maximum number of * results that can be returned - * + * * @param startAt if non-null, defines the first issue to * return * @@ -1611,7 +1611,7 @@ public class Issue extends Resource { public User getReporter() { return reporter; } - + public List getRemoteLinks() throws JiraException { JSONArray obj; try { @@ -1696,5 +1696,17 @@ public class Issue extends Resource { return updatedDate; } + public boolean delete(final boolean deleteSubtasks) throws JiraException { + boolean result; + try { + URI uri = restclient.buildURI(getBaseUri() + "issue/" + this.key, new HashMap() {{ + put("deleteSubtasks", String.valueOf(deleteSubtasks)); + }}); + result = (restclient.delete(uri) == null); + } catch (Exception ex) { + throw new JiraException("Failed to delete issue " + key, ex); + } + return result; + } } diff --git a/src/test/java/net/rcarz/jiraclient/IssueTest.java b/src/test/java/net/rcarz/jiraclient/IssueTest.java index f50e6e6..c95507d 100644 --- a/src/test/java/net/rcarz/jiraclient/IssueTest.java +++ b/src/test/java/net/rcarz/jiraclient/IssueTest.java @@ -5,6 +5,7 @@ import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import java.net.URI; import java.util.List; import java.util.Map; @@ -16,6 +17,9 @@ import org.joda.time.DateTimeZone; import org.junit.Assert; import org.junit.Test; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; + public class IssueTest { /** @@ -199,4 +203,16 @@ public class IssueTest { } + /** + * false is bu default so we test positive case only + */ + @Test + public void testDelete() throws Exception { + RestClient restClient = mock(RestClient.class); + URI uri = new URI("DUMMY"); + when(restClient.buildURI(anyString(), any(Map.class))).thenReturn(uri); + when(restClient.delete(eq(uri))).thenReturn(null); + Issue issue = new Issue(restClient, Utils.getTestIssue()); + Assert.assertTrue(issue.delete(true)); + } } From c8802583aa9c36942fcca8cea6106c9f5b4dfcf4 Mon Sep 17 00:00:00 2001 From: Bob Carroll Date: Mon, 30 May 2016 13:11:12 -0700 Subject: [PATCH 11/14] fixed tabbing --- src/main/java/net/rcarz/jiraclient/Issue.java | 388 +++++++++--------- .../java/net/rcarz/jiraclient/JiraClient.java | 20 +- 2 files changed, 204 insertions(+), 204 deletions(-) diff --git a/src/main/java/net/rcarz/jiraclient/Issue.java b/src/main/java/net/rcarz/jiraclient/Issue.java index 371649d..38dcd81 100644 --- a/src/main/java/net/rcarz/jiraclient/Issue.java +++ b/src/main/java/net/rcarz/jiraclient/Issue.java @@ -528,131 +528,131 @@ public class Issue extends Resource { } - /** + /** * Iterates over all issues in the query by getting the next page of * issues when the iterator reaches the last of the current page. */ private static class IssueIterator implements Iterator { - private Iterator currentPage; - private RestClient restclient; - private Issue nextIssue; + private Iterator currentPage; + private RestClient restclient; + private Issue nextIssue; private Integer maxResults = -1; - private String jql; - private String includedFields; - private String expandFields; - private Integer startAt; - private List issues; - private int total; - + private String jql; + private String includedFields; + private String expandFields; + private Integer startAt; + private List issues; + private int total; + public IssueIterator(RestClient restclient, String jql, String includedFields, String expandFields, Integer maxResults, Integer startAt) throws JiraException { - this.restclient = restclient; - this.jql = jql; - this.includedFields = includedFields; - this.expandFields = expandFields; - this.maxResults = maxResults; - this.startAt = startAt; + this.restclient = restclient; + this.jql = jql; + this.includedFields = includedFields; + this.expandFields = expandFields; + this.maxResults = maxResults; + this.startAt = startAt; + } + + @Override + public boolean hasNext() { + if (nextIssue != null) { + return true; + } + try { + nextIssue = getNextIssue(); + } catch (JiraException e) { + throw new RuntimeException(e); + } + return nextIssue != null; } - - @Override - public boolean hasNext() { - if (nextIssue != null) { - return true; - } - try { - nextIssue = getNextIssue(); - } catch (JiraException e) { - throw new RuntimeException(e); - } - return nextIssue != null; - } - @Override - public Issue next() { - if (! hasNext()) { - throw new NoSuchElementException(); - } - Issue result = nextIssue; - nextIssue = null; - return result; - } + @Override + public Issue next() { + if (! hasNext()) { + throw new NoSuchElementException(); + } + Issue result = nextIssue; + nextIssue = null; + return result; + } - @Override - public void remove() { - throw new UnsupportedOperationException("Method remove() not support for class " + this.getClass().getName()); - } + @Override + public void remove() { + throw new UnsupportedOperationException("Method remove() not support for class " + this.getClass().getName()); + } - /** - * Gets the next issue, returning null if none more available - * Will ask the next set of issues from the server if the end of the current list of issues is reached. - * - * @return the next issue, null if none more available - * @throws JiraException - */ - private Issue getNextIssue() throws JiraException { - // first call - if (currentPage == null) { - currentPage = getNextIssues().iterator(); - if (currentPage == null || !currentPage.hasNext()) { - return null; - } else { - return currentPage.next(); - } - } - - // check if we need to get the next set of issues - if (! currentPage.hasNext()) { - currentPage = getNextIssues().iterator(); - } + /** + * Gets the next issue, returning null if none more available + * Will ask the next set of issues from the server if the end of the current list of issues is reached. + * + * @return the next issue, null if none more available + * @throws JiraException + */ + private Issue getNextIssue() throws JiraException { + // first call + if (currentPage == null) { + currentPage = getNextIssues().iterator(); + if (currentPage == null || !currentPage.hasNext()) { + return null; + } else { + return currentPage.next(); + } + } + + // check if we need to get the next set of issues + if (! currentPage.hasNext()) { + currentPage = getNextIssues().iterator(); + } - // return the next item if available - if (currentPage.hasNext()) { - return currentPage.next(); - } else { - return null; - } - } + // return the next item if available + if (currentPage.hasNext()) { + return currentPage.next(); + } else { + return null; + } + } - /** - * Execute the query to get the next set of issues. - * Also sets the startAt, maxMresults, total and issues fields, - * so that the SearchResult can access them. - * - * @return the next set of issues. - * @throws JiraException - */ - private List getNextIssues() throws JiraException { - if (issues == null) { - startAt = Integer.valueOf(0); - } else { - startAt = startAt + issues.size(); - } + /** + * Execute the query to get the next set of issues. + * Also sets the startAt, maxMresults, total and issues fields, + * so that the SearchResult can access them. + * + * @return the next set of issues. + * @throws JiraException + */ + private List getNextIssues() throws JiraException { + if (issues == null) { + startAt = Integer.valueOf(0); + } else { + startAt = startAt + issues.size(); + } - JSON result = null; + JSON result = null; - try { - URI searchUri = createSearchURI(restclient, jql, includedFields, - expandFields, maxResults, startAt); - result = restclient.get(searchUri); - } catch (Exception ex) { - throw new JiraException("Failed to search issues", ex); - } + try { + URI searchUri = createSearchURI(restclient, jql, includedFields, + expandFields, maxResults, startAt); + result = restclient.get(searchUri); + } catch (Exception ex) { + throw new JiraException("Failed to search issues", ex); + } - if (!(result instanceof JSONObject)) { - throw new JiraException("JSON payload is malformed"); - } + if (!(result instanceof JSONObject)) { + throw new JiraException("JSON payload is malformed"); + } - - Map map = (Map) result; - - this.startAt = Field.getInteger(map.get("startAt")); - this.maxResults = Field.getInteger(map.get("maxResults")); - this.total = Field.getInteger(map.get("total")); - this.issues = Field.getResourceArray(Issue.class, map.get("issues"), restclient); - return issues; - } + + Map map = (Map) result; + + this.startAt = Field.getInteger(map.get("startAt")); + this.maxResults = Field.getInteger(map.get("maxResults")); + this.total = Field.getInteger(map.get("total")); + this.issues = Field.getResourceArray(Issue.class, map.get("issues"), restclient); + return issues; + } } @@ -675,38 +675,38 @@ public class Issue extends Resource { public int max = 0; public int total = 0; public List issues = null; - private RestClient restclient; - private String jql; - private String includedFields; - private String expandFields; - private Integer startAt; - private IssueIterator issueIterator; + private RestClient restclient; + private String jql; + private String includedFields; + private String expandFields; + private Integer startAt; + private IssueIterator issueIterator; - public SearchResult(RestClient restclient, String jql, - String includedFields, String expandFields, Integer maxResults, Integer startAt) throws JiraException { - this.restclient = restclient; - this.jql = jql; - this.includedFields = includedFields; - this.expandFields = expandFields; - initSearchResult(maxResults, start); + public SearchResult(RestClient restclient, String jql, + String includedFields, String expandFields, Integer maxResults, Integer startAt) throws JiraException { + this.restclient = restclient; + this.jql = jql; + this.includedFields = includedFields; + this.expandFields = expandFields; + initSearchResult(maxResults, start); } private void initSearchResult(Integer maxResults, Integer start) throws JiraException { - this.issueIterator = new IssueIterator(restclient, jql, includedFields, expandFields, maxResults, startAt); - this.issueIterator.hasNext(); - this.max = issueIterator.maxResults; - this.start = issueIterator.startAt; - this.issues = issueIterator.issues; - this.total = issueIterator.total; - } + this.issueIterator = new IssueIterator(restclient, jql, includedFields, expandFields, maxResults, startAt); + this.issueIterator.hasNext(); + this.max = issueIterator.maxResults; + this.start = issueIterator.startAt; + this.issues = issueIterator.issues; + this.total = issueIterator.total; + } - /** + /** * All issues found. * * @return All issues found. */ public Iterator iterator() { - return issueIterator; + return issueIterator; } } @@ -860,10 +860,10 @@ public class Issue extends Resource { JSON result = null; try { - Map params = new HashMap(); - params.put("expand", "projects.issuetypes.fields"); - params.put("projectKeys", pval); - params.put("issuetypeNames", itval); + Map params = new HashMap(); + params.put("expand", "projects.issuetypes.fields"); + params.put("projectKeys", pval); + params.put("issuetypeNames", itval); URI createuri = restclient.buildURI( getBaseUri() + "issue/createmeta", params); @@ -918,8 +918,8 @@ public class Issue extends Resource { JSON result = null; try { - Map params = new HashMap(); - params.put("expand", "transitions.fields"); + Map params = new HashMap(); + params.put("expand", "transitions.fields"); URI transuri = restclient.buildURI( getRestUri(key) + "/transitions",params); result = restclient.get(transuri); @@ -1009,24 +1009,24 @@ public class Issue extends Resource { } /** - * Removes an attachments. - * - * @param attachmentId attachment id to remove - * - * @throws JiraException when the attachment removal fails - */ - public void removeAttachment(String attachmentId) throws JiraException { - - if (attachmentId == null) { - throw new NullPointerException("attachmentId may not be null"); - } - - try { - restclient.delete(getBaseUri() + "attachment/" + attachmentId); - } catch (Exception ex) { - throw new JiraException("Failed remove attachment " + attachmentId, ex); - } - } + * Removes an attachments. + * + * @param attachmentId attachment id to remove + * + * @throws JiraException when the attachment removal fails + */ + public void removeAttachment(String attachmentId) throws JiraException { + + if (attachmentId == null) { + throw new NullPointerException("attachmentId may not be null"); + } + + try { + restclient.delete(getBaseUri() + "attachment/" + attachmentId); + } catch (Exception ex) { + throw new JiraException("Failed remove attachment " + attachmentId, ex); + } + } /** * Adds a comment to this issue. @@ -1271,9 +1271,9 @@ public class Issue extends Resource { Map queryParams = new HashMap(); queryParams.put("fields", includedFields); - if (expand != null) { - queryParams.put("expand", expand); - } + if (expand != null) { + queryParams.put("expand", expand); + } return new Issue(restclient, realGet(restclient, key, queryParams)); } @@ -1357,19 +1357,19 @@ public class Issue extends Resource { String includedFields, String expandFields, Integer maxResults, Integer startAt) throws JiraException { - SearchResult sr = new SearchResult(restclient, jql, includedFields, expandFields, maxResults, startAt); + SearchResult sr = new SearchResult(restclient, jql, includedFields, expandFields, maxResults, startAt); return sr; } - private static JSON executeSearch(RestClient restclient, String jql, - String includedFields, String expandFields, Integer maxResults, - Integer startAt) throws JiraException { + private static JSON executeSearch(RestClient restclient, String jql, + String includedFields, String expandFields, Integer maxResults, + Integer startAt) throws JiraException { JSON result = null; try { URI searchUri = createSearchURI(restclient, jql, includedFields, - expandFields, maxResults, startAt); + expandFields, maxResults, startAt); result = restclient.get(searchUri); } catch (Exception ex) { throw new JiraException("Failed to search issues", ex); @@ -1378,42 +1378,42 @@ public class Issue extends Resource { if (!(result instanceof JSONObject)) { throw new JiraException("JSON payload is malformed"); } - return result; - } + return result; + } - /** - * Creates the URI to execute a jql search. - * - * @param restclient - * @param jql - * @param includedFields - * @param expandFields - * @param maxResults - * @param startAt - * @return the URI to execute a jql search. - * @throws URISyntaxException - */ - private static URI createSearchURI(RestClient restclient, String jql, - String includedFields, String expandFields, Integer maxResults, - Integer startAt) throws URISyntaxException { - Map queryParams = new HashMap(); - queryParams.put("jql", jql); - if(maxResults != null){ - queryParams.put("maxResults", String.valueOf(maxResults)); - } - if (includedFields != null) { - queryParams.put("fields", includedFields); - } - if (expandFields != null) { - queryParams.put("expand", expandFields); - } - if (startAt != null) { - queryParams.put("startAt", String.valueOf(startAt)); - } + /** + * Creates the URI to execute a jql search. + * + * @param restclient + * @param jql + * @param includedFields + * @param expandFields + * @param maxResults + * @param startAt + * @return the URI to execute a jql search. + * @throws URISyntaxException + */ + private static URI createSearchURI(RestClient restclient, String jql, + String includedFields, String expandFields, Integer maxResults, + Integer startAt) throws URISyntaxException { + Map queryParams = new HashMap(); + queryParams.put("jql", jql); + if(maxResults != null){ + queryParams.put("maxResults", String.valueOf(maxResults)); + } + if (includedFields != null) { + queryParams.put("fields", includedFields); + } + if (expandFields != null) { + queryParams.put("expand", expandFields); + } + if (startAt != null) { + queryParams.put("startAt", String.valueOf(startAt)); + } - URI searchUri = restclient.buildURI(getBaseUri() + "search", queryParams); - return searchUri; - } + URI searchUri = restclient.buildURI(getBaseUri() + "search", queryParams); + return searchUri; + } /** * Reloads issue data from the JIRA server (issue includes all navigable diff --git a/src/main/java/net/rcarz/jiraclient/JiraClient.java b/src/main/java/net/rcarz/jiraclient/JiraClient.java index 067e4aa..9c81d49 100644 --- a/src/main/java/net/rcarz/jiraclient/JiraClient.java +++ b/src/main/java/net/rcarz/jiraclient/JiraClient.java @@ -61,7 +61,7 @@ public class JiraClient { * @throws JiraException */ public JiraClient(String uri, ICredentials creds) throws JiraException { - this(null, uri, creds); + this(null, uri, creds); } /** @@ -73,19 +73,19 @@ public class JiraClient { * @throws JiraException */ public JiraClient(HttpClient httpClient, String uri, ICredentials creds) throws JiraException { - if (httpClient == null) { - PoolingClientConnectionManager connManager = new PoolingClientConnectionManager(); - connManager.setDefaultMaxPerRoute(20); - connManager.setMaxTotal(40); - httpClient = new DefaultHttpClient(connManager); - } + if (httpClient == null) { + PoolingClientConnectionManager connManager = new PoolingClientConnectionManager(); + connManager.setDefaultMaxPerRoute(20); + connManager.setMaxTotal(40); + httpClient = new DefaultHttpClient(connManager); + } restclient = new RestClient(httpClient, creds, URI.create(uri)); if (creds != null) { username = creds.getLogonName(); - //intialize connection if required - creds.initialize(restclient); + //intialize connection if required + creds.initialize(restclient); } } @@ -368,7 +368,7 @@ public class JiraClient { startAt); } - /** + /** * Retrieve the jira filter with the supplied id. * @param id id of the filter to retreive. * @return The Jira filter with the supplied id From e3f68bf05dab435b2fc84db58de0a431ba1afea4 Mon Sep 17 00:00:00 2001 From: Bob Carroll Date: Mon, 30 May 2016 15:03:09 -0700 Subject: [PATCH 12/14] cleaned up search implementation --- README.md | 6 + src/main/java/net/rcarz/jiraclient/Issue.java | 120 +++++------------- .../java/net/rcarz/jiraclient/JiraClient.java | 32 +++-- 3 files changed, 54 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index 483eeea..d4e5b4c 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,12 @@ public class Example { for (Issue i : sr.issues) System.out.println("Result: " + i); + /* Search with paging (optionally 10 issues at a time). There are optional + arguments for including/expanding fields, and page size/start. */ + Issue.SearchResult sr = jira.searchIssues("project IN (GOTHAM) ORDER BY id"); + while (sr.iterator().hasNext()) + System.out.println("Result: " + sr.iterator().next()); + } catch (JiraException ex) { System.err.println(ex.getMessage()); diff --git a/src/main/java/net/rcarz/jiraclient/Issue.java b/src/main/java/net/rcarz/jiraclient/Issue.java index 38dcd81..8d195d4 100644 --- a/src/main/java/net/rcarz/jiraclient/Issue.java +++ b/src/main/java/net/rcarz/jiraclient/Issue.java @@ -544,9 +544,9 @@ public class Issue extends Resource { private List issues; private int total; - public IssueIterator(RestClient restclient, String jql, - String includedFields, String expandFields, Integer maxResults, Integer startAt) - throws JiraException { + public IssueIterator(RestClient restclient, String jql, String includedFields, + String expandFields, Integer maxResults, Integer startAt) + throws JiraException { this.restclient = restclient; this.jql = jql; this.includedFields = includedFields; @@ -578,15 +578,16 @@ public class Issue extends Resource { return result; } - @Override public void remove() { - throw new UnsupportedOperationException("Method remove() not support for class " + this.getClass().getName()); + throw new UnsupportedOperationException("Method remove() not support for class " + + this.getClass().getName()); } /** * Gets the next issue, returning null if none more available - * Will ask the next set of issues from the server if the end of the current list of issues is reached. + * Will ask the next set of issues from the server if the end + * of the current list of issues is reached. * * @return the next issue, null if none more available * @throws JiraException @@ -624,9 +625,9 @@ public class Issue extends Resource { * @throws JiraException */ private List getNextIssues() throws JiraException { - if (issues == null) { + if (issues == null && startAt == null) { startAt = Integer.valueOf(0); - } else { + } else if (issues != null) { startAt = startAt + issues.size(); } @@ -653,7 +654,6 @@ public class Issue extends Resource { this.issues = Field.getResourceArray(Issue.class, map.get("issues"), restclient); return issues; } - } /** @@ -675,24 +675,20 @@ public class Issue extends Resource { public int max = 0; public int total = 0; public List issues = null; - private RestClient restclient; - private String jql; - private String includedFields; - private String expandFields; - private Integer startAt; private IssueIterator issueIterator; - public SearchResult(RestClient restclient, String jql, - String includedFields, String expandFields, Integer maxResults, Integer startAt) throws JiraException { - this.restclient = restclient; - this.jql = jql; - this.includedFields = includedFields; - this.expandFields = expandFields; - initSearchResult(maxResults, start); - } - - private void initSearchResult(Integer maxResults, Integer start) throws JiraException { - this.issueIterator = new IssueIterator(restclient, jql, includedFields, expandFields, maxResults, startAt); + public SearchResult(RestClient restclient, String jql, String includedFields, + String expandFields, Integer maxResults, Integer startAt) + throws JiraException { + this.issueIterator = new IssueIterator( + restclient, + jql, + includedFields, + expandFields, + maxResults, + startAt + ); + /* backwards compatibility shim - first page only */ this.issueIterator.hasNext(); this.max = issueIterator.maxResults; this.start = issueIterator.startAt; @@ -1277,50 +1273,6 @@ public class Issue extends Resource { return new Issue(restclient, realGet(restclient, key, queryParams)); } - /** - * Search for issues with the given query. - * - * @param restclient REST client instance - * - * @param jql JQL statement - * - * @return a search result structure with results (issues include all - * navigable fields) - * - * @throws JiraException when the search fails - */ - public static SearchResult search(RestClient restclient, String jql) - throws JiraException { - return search(restclient, jql, null, null); - } - - /** - * Search for issues with the given query and specify which fields to - * retrieve. - * - * @param restclient REST client instance - * - * @param jql JQL statement - * - * @param includedFields Specifies which issue fields will be included in - * the result. - *
    Some examples how this parameter works: - *
      - *
    • *all - include all fields
    • - *
    • *navigable - include just navigable fields
    • - *
    • summary,comment - include just the summary and comments
    • - *
    • *all,-comment - include all fields
    • - *
    - * - * @return a search result structure with results - * - * @throws JiraException when the search fails - */ - public static SearchResult search(RestClient restclient, String jql, String includedFields, Integer maxResults) - throws JiraException { - return search(restclient, jql, includedFields, null, maxResults, null); - } - /** * Search for issues with the given query and specify which fields to * retrieve. If the total results is bigger than the maximum returned @@ -1354,31 +1306,17 @@ public class Issue extends Resource { * @throws JiraException when the search fails */ public static SearchResult search(RestClient restclient, String jql, - String includedFields, String expandFields, Integer maxResults, Integer startAt) - throws JiraException { - - SearchResult sr = new SearchResult(restclient, jql, includedFields, expandFields, maxResults, startAt); - - return sr; - } - - private static JSON executeSearch(RestClient restclient, String jql, String includedFields, String expandFields, Integer maxResults, Integer startAt) throws JiraException { - JSON result = null; - try { - URI searchUri = createSearchURI(restclient, jql, includedFields, - expandFields, maxResults, startAt); - result = restclient.get(searchUri); - } catch (Exception ex) { - throw new JiraException("Failed to search issues", ex); - } - - if (!(result instanceof JSONObject)) { - throw new JiraException("JSON payload is malformed"); - } - return result; + return new SearchResult( + restclient, + jql, + includedFields, + expandFields, + maxResults, + startAt + ); } /** diff --git a/src/main/java/net/rcarz/jiraclient/JiraClient.java b/src/main/java/net/rcarz/jiraclient/JiraClient.java index 9c81d49..276b8d7 100644 --- a/src/main/java/net/rcarz/jiraclient/JiraClient.java +++ b/src/main/java/net/rcarz/jiraclient/JiraClient.java @@ -193,7 +193,7 @@ public class JiraClient { public Issue.SearchResult searchIssues(String jql) throws JiraException { - return Issue.search(restclient, jql, null, null); + return searchIssues(jql, null, null, null, null); } /** @@ -210,7 +210,7 @@ public class JiraClient { public Issue.SearchResult searchIssues(String jql, Integer maxResults) throws JiraException { - return Issue.search(restclient, jql, null, maxResults); + return searchIssues(jql, null, null, maxResults, null); } /** @@ -237,7 +237,7 @@ public class JiraClient { public Issue.SearchResult searchIssues(String jql, String includedFields) throws JiraException { - return Issue.search(restclient, jql, includedFields, null); + return searchIssues(jql, includedFields, null, null, null); } /** @@ -262,10 +262,10 @@ public class JiraClient { * * @throws JiraException when the search fails */ - public Issue.SearchResult searchIssues(String jql, String includedFields, String expandFields) - throws JiraException { + public Issue.SearchResult searchIssues(String jql, String includedFields, + String expandFields) throws JiraException { - return Issue.search(restclient, jql, includedFields, expandFields, null, null); + return searchIssues(jql, includedFields, expandFields, null, null); } /** @@ -293,7 +293,7 @@ public class JiraClient { public Issue.SearchResult searchIssues(String jql, String includedFields, Integer maxResults) throws JiraException { - return Issue.search(restclient, jql, includedFields, maxResults); + return searchIssues(jql, includedFields, null, maxResults, null); } /** @@ -327,8 +327,7 @@ public class JiraClient { public Issue.SearchResult searchIssues(String jql, String includedFields, Integer maxResults, Integer startAt) throws JiraException { - return Issue.search(restclient, jql, includedFields, null, maxResults, - startAt); + return searchIssues(jql, includedFields, null, maxResults, startAt); } /** @@ -361,11 +360,18 @@ public class JiraClient { * * @throws JiraException when the search fails */ - public Issue.SearchResult searchIssues(String jql, String includedFields, String expandFields, - Integer maxResults, Integer startAt) throws JiraException { + public Issue.SearchResult searchIssues(String jql, String includedFields, + String expandFields, Integer maxResults, + Integer startAt) throws JiraException { - return Issue.search(restclient, jql, includedFields, expandFields, maxResults, - startAt); + return Issue.search( + restclient, + jql, + includedFields, + expandFields, + maxResults, + startAt + ); } /** From dc5ed2e3f1394742d54d2ad48aaa1cd6082b5c06 Mon Sep 17 00:00:00 2001 From: Bob Carroll Date: Mon, 30 May 2016 15:16:28 -0700 Subject: [PATCH 13/14] fixed NPE when only id,key fields are requested on search #140 --- src/main/java/net/rcarz/jiraclient/Issue.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/net/rcarz/jiraclient/Issue.java b/src/main/java/net/rcarz/jiraclient/Issue.java index 8d195d4..d5154a5 100644 --- a/src/main/java/net/rcarz/jiraclient/Issue.java +++ b/src/main/java/net/rcarz/jiraclient/Issue.java @@ -812,6 +812,8 @@ public class Issue extends Resource { key = Field.getString(map.get("key")); fields = (Map)map.get("fields"); + if (fields == null) + return; assignee = Field.getResource(User.class, fields.get(Field.ASSIGNEE), restclient); attachments = Field.getResourceArray(Attachment.class, fields.get(Field.ATTACHMENT), restclient); From 74c21b5a1f660ad479c4f73a5ed266de2b3ceb40 Mon Sep 17 00:00:00 2001 From: Bob Carroll Date: Mon, 30 May 2016 16:17:04 -0700 Subject: [PATCH 14/14] implemented comment updating --- README.md | 5 ++ .../java/net/rcarz/jiraclient/Comment.java | 58 ++++++++++++++++++- src/main/java/net/rcarz/jiraclient/Field.java | 57 ++++++++++++++++-- src/main/java/net/rcarz/jiraclient/Issue.java | 18 ++++-- 4 files changed, 126 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d4e5b4c..404c6ae 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,11 @@ public class Example { /* Now let's start progress on this issue. */ issue.transition().execute("Start Progress"); + /* Add the first comment and update it */ + Comment comment = issue.addComment("I am a comment!"); + comment.update("I am the first comment!"); + issue.getComments().get(0).update("this works too!"); + /* Pretend customfield_1234 is a text field. Get the raw field value... */ Object cfvalue = issue.getField("customfield_1234"); diff --git a/src/main/java/net/rcarz/jiraclient/Comment.java b/src/main/java/net/rcarz/jiraclient/Comment.java index 7d92824..ef53a73 100644 --- a/src/main/java/net/rcarz/jiraclient/Comment.java +++ b/src/main/java/net/rcarz/jiraclient/Comment.java @@ -30,6 +30,7 @@ import java.util.Map; */ public class Comment extends Resource { + private String issueKey = null; private User author = null; private String body = null; private Date created = null; @@ -42,9 +43,10 @@ public class Comment extends Resource { * @param restclient REST client instance * @param json JSON payload */ - protected Comment(RestClient restclient, JSONObject json) { + protected Comment(RestClient restclient, JSONObject json, String issueKey) { super(restclient); + this.issueKey = issueKey; if (json != null) deserialise(json); } @@ -86,7 +88,59 @@ public class Comment extends Resource { if (!(result instanceof JSONObject)) throw new JiraException("JSON payload is malformed"); - return new Comment(restclient, (JSONObject)result); + return new Comment(restclient, (JSONObject)result, issue); + } + + /** + * Updates the comment body. + * + * @param issue associated issue record + * @param body Comment text + * + * @throws JiraException when the comment update fails + */ + public void update(String body) throws JiraException { + update(body, null, null); + } + + /** + * Updates the comment body with limited visibility. + * + * @param issue associated issue record + * @param body Comment text + * @param visType Target audience type (role or group) + * @param visName Name of the role or group to limit visibility to + * + * @throws JiraException when the comment update fails + */ + public void update(String body, String visType, String visName) + throws JiraException { + + JSONObject req = new JSONObject(); + req.put("body", body); + + if (visType != null && visName != null) { + JSONObject vis = new JSONObject(); + vis.put("type", visType); + vis.put("value", visName); + + req.put("visibility", vis); + } + + JSON result = null; + + try { + String issueUri = getBaseUri() + "issue/" + issueKey; + result = restclient.put(issueUri + "/comment/" + id, req); + } catch (Exception ex) { + throw new JiraException("Failed add update comment " + id, ex); + } + + if (!(result instanceof JSONObject)) { + throw new JiraException("JSON payload is malformed"); + } + + deserialise((JSONObject) result); } @Override diff --git a/src/main/java/net/rcarz/jiraclient/Field.java b/src/main/java/net/rcarz/jiraclient/Field.java index c0e3700..0ab62f2 100644 --- a/src/main/java/net/rcarz/jiraclient/Field.java +++ b/src/main/java/net/rcarz/jiraclient/Field.java @@ -176,14 +176,22 @@ public final class Field { * * @param c a JSONObject instance * @param restclient REST client instance + * @param issueKey key of the parent issue * * @return a list of comments found in c */ - public static List getComments(Object c, RestClient restclient) { + public static List getComments(Object c, RestClient restclient, + String issueKey) { List results = new ArrayList(); - if (c instanceof JSONObject && !((JSONObject)c).isNullObject()) - results = getResourceArray(Comment.class, ((Map)c).get("comments"), restclient); + if (c instanceof JSONObject && !((JSONObject)c).isNullObject()) { + results = getResourceArray( + Comment.class, + ((Map)c).get("comments"), + restclient, + issueKey + ); + } return results; } @@ -328,6 +336,22 @@ public final class Field { public static T getResource( Class type, Object r, RestClient restclient) { + return getResource(type, r, restclient, null); + } + + /** + * Gets a JIRA resource from the given object. + * + * @param type Resource data type + * @param r a JSONObject instance + * @param restclient REST client instance + * @param parentId id/key of the parent resource + * + * @return a Resource instance or null if r isn't a JSONObject instance + */ + public static T getResource( + Class type, Object r, RestClient restclient, String parentId) { + T result = null; if (r instanceof JSONObject && !((JSONObject)r).isNullObject()) { @@ -340,7 +364,7 @@ public final class Field { else if (type == ChangeLogItem.class) result = (T)new ChangeLogItem(restclient, (JSONObject)r); else if (type == Comment.class) - result = (T)new Comment(restclient, (JSONObject)r); + result = (T)new Comment(restclient, (JSONObject)r, parentId); else if (type == Component.class) result = (T)new Component(restclient, (JSONObject)r); else if (type == CustomFieldOption.class) @@ -430,11 +454,34 @@ public final class Field { public static List getResourceArray( Class type, Object ra, RestClient restclient) { + return getResourceArray(type, ra, restclient, null); + } + + /** + * Gets a list of JIRA resources from the given object. + * + * @param type Resource data type + * @param ra a JSONArray instance + * @param restclient REST client instance + * @param parentId id/key of the parent resource + * + * @return a list of Resources found in ra + */ + public static List getResourceArray( + Class type, Object ra, RestClient restclient, String parentId) { + List results = new ArrayList(); if (ra instanceof JSONArray) { for (Object v : (JSONArray)ra) { - T item = getResource(type, v, restclient); + T item = null; + + if (parentId != null) { + item = getResource(type, v, restclient, parentId); + } else { + item = getResource(type, v, restclient); + } + if (item != null) results.add(item); } diff --git a/src/main/java/net/rcarz/jiraclient/Issue.java b/src/main/java/net/rcarz/jiraclient/Issue.java index d5154a5..1cb586a 100644 --- a/src/main/java/net/rcarz/jiraclient/Issue.java +++ b/src/main/java/net/rcarz/jiraclient/Issue.java @@ -818,7 +818,7 @@ public class Issue extends Resource { assignee = Field.getResource(User.class, fields.get(Field.ASSIGNEE), restclient); attachments = Field.getResourceArray(Attachment.class, fields.get(Field.ATTACHMENT), restclient); changeLog = Field.getResource(ChangeLog.class, map.get(Field.CHANGE_LOG), restclient); - comments = Field.getComments(fields.get(Field.COMMENT), restclient); + comments = Field.getComments(fields.get(Field.COMMENT), restclient, key); components = Field.getResourceArray(Component.class, fields.get(Field.COMPONENTS), restclient); description = Field.getString(fields.get(Field.DESCRIPTION)); dueDate = Field.getDate(fields.get(Field.DUE_DATE)); @@ -1033,8 +1033,8 @@ public class Issue extends Resource { * * @throws JiraException when the comment creation fails */ - public void addComment(String body) throws JiraException { - addComment(body, null, null); + public Comment addComment(String body) throws JiraException { + return addComment(body, null, null); } /** @@ -1046,7 +1046,7 @@ public class Issue extends Resource { * * @throws JiraException when the comment creation fails */ - public void addComment(String body, String visType, String visName) + public Comment addComment(String body, String visType, String visName) throws JiraException { JSONObject req = new JSONObject(); @@ -1060,11 +1060,19 @@ public class Issue extends Resource { req.put("visibility", vis); } + JSON result = null; + try { - restclient.post(getRestUri(key) + "/comment", req); + result = restclient.post(getRestUri(key) + "/comment", req); } catch (Exception ex) { throw new JiraException("Failed add comment to issue " + key, ex); } + + if (!(result instanceof JSONObject)) { + throw new JiraException("JSON payload is malformed"); + } + + return new Comment(restclient, (JSONObject) result, key); } /**