diff --git a/system-x/services/jira/README.md b/system-x/services/jira/README.md
new file mode 100644
index 000000000..f7947beb7
--- /dev/null
+++ b/system-x/services/jira/README.md
@@ -0,0 +1,21 @@
+# TNB :: System-X :: Services :: Jira
+
+Jira integration service for the TNB test framework. Provides CRUD operations on Jira issues via the [Jira REST API v3](https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/).
+
+## API Reference
+
+The OpenAPI specification for the Jira REST API is available at:
+https://developer.atlassian.com/cloud/jira/platform/swagger-v3.v3.json
+
+## Available Operations
+
+| Method | Description |
+|--------|-------------|
+| `createIssue(projectKey, issueSummary)` | Create a Bug issue, returns issue key |
+| `deleteIssue(issueKey)` | Delete an issue and its subtasks |
+| `getIssue(issueKey)` | Get issue details |
+| `getIssues(jql)` | Search issues using JQL |
+| `getIssues(project, customJQL)` | Search issues within a project |
+| `getComments(issueKey)` | Get all comments on an issue |
+| `addComment(issueKey, content)` | Add a comment to an issue |
+| `setTransition(issueKey, transitionId)` | Transition an issue to a new status |
diff --git a/system-x/services/jira/pom.xml b/system-x/services/jira/pom.xml
index 334e76773..d736abfcf 100644
--- a/system-x/services/jira/pom.xml
+++ b/system-x/services/jira/pom.xml
@@ -11,59 +11,6 @@
system-x-jira
- 1.0-SNAPSHOT
TNB :: System-X :: Services :: Jira
-
- 6.0.2
- 6.1.2
-
- 3.15.6.Final
-
-
-
-
- com.atlassian.jira
- jira-rest-java-client-core
- ${jira.client.version}
-
-
- org.glassfish.jersey.core
- *
-
-
- org.glassfish.jersey.media
- *
-
-
-
-
- io.atlassian.fugue
- fugue
- ${fugue.version}
-
-
-
- javax.ws.rs
- javax.ws.rs-api
- 2.1.1
-
-
- org.jboss.resteasy
- resteasy-client
- ${resteasy.version}
-
-
- org.jboss.resteasy
- resteasy-jaxb-provider
- ${resteasy.version}
-
-
-
-
-
- atlassian
- https://maven.atlassian.com/content/repositories/atlassian-public/
-
-
diff --git a/system-x/services/jira/src/main/java/software/tnb/jira/service/Jira.java b/system-x/services/jira/src/main/java/software/tnb/jira/service/Jira.java
index ff5e225b6..f4cc02073 100644
--- a/system-x/services/jira/src/main/java/software/tnb/jira/service/Jira.java
+++ b/system-x/services/jira/src/main/java/software/tnb/jira/service/Jira.java
@@ -1,5 +1,6 @@
package software.tnb.jira.service;
+import software.tnb.common.client.NoClient;
import software.tnb.common.service.Service;
import software.tnb.jira.account.JiraAccount;
import software.tnb.jira.validation.JiraValidation;
@@ -9,41 +10,19 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import com.atlassian.jira.rest.client.api.JiraRestClient;
-import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory;
import com.google.auto.service.AutoService;
-import java.net.URI;
-import java.net.URISyntaxException;
-
@AutoService(Jira.class)
-public class Jira extends Service {
+public class Jira extends Service {
private static final Logger LOG = LoggerFactory.getLogger(Jira.class);
- @Override
- protected JiraRestClient client() {
- if (client == null) {
- LOG.debug("Creating new Jira client");
- try {
- client = new AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(new URI(account().getJiraUrl()),
- account().getUsername(), account().getPassword());
- } catch (URISyntaxException e) {
- throw new RuntimeException("Unable to create jira client", e);
- }
-
- }
- return client;
- }
-
@Override
public void afterAll(ExtensionContext context) throws Exception {
- client.close();
- client = null;
}
@Override
public void beforeAll(ExtensionContext context) throws Exception {
LOG.debug("Creating new Jira validation");
- validation = new JiraValidation(client());
+ validation = new JiraValidation(account());
}
}
diff --git a/system-x/services/jira/src/main/java/software/tnb/jira/validation/JiraValidation.java b/system-x/services/jira/src/main/java/software/tnb/jira/validation/JiraValidation.java
index 8f36c8bb1..b54977cd8 100644
--- a/system-x/services/jira/src/main/java/software/tnb/jira/validation/JiraValidation.java
+++ b/system-x/services/jira/src/main/java/software/tnb/jira/validation/JiraValidation.java
@@ -1,30 +1,44 @@
package software.tnb.jira.validation;
+import software.tnb.common.utils.HTTPUtils;
import software.tnb.common.validation.Validation;
+import software.tnb.jira.account.JiraAccount;
import software.tnb.jira.validation.model.Issue;
+import org.json.JSONArray;
+import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import com.atlassian.jira.rest.client.api.JiraRestClient;
-import com.atlassian.jira.rest.client.api.domain.Attachment;
-import com.atlassian.jira.rest.client.api.domain.Comment;
-import com.atlassian.jira.rest.client.api.domain.IssueType;
-import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder;
-import com.atlassian.jira.rest.client.api.domain.input.TransitionInput;
-
-import java.net.URI;
+import java.util.ArrayList;
import java.util.List;
-import java.util.Optional;
-import java.util.stream.StreamSupport;
+import java.util.Map;
+
+import okhttp3.Credentials;
+import okhttp3.MediaType;
+import okhttp3.RequestBody;
public class JiraValidation implements Validation {
private static final Logger LOG = LoggerFactory.getLogger(JiraValidation.class);
+ private static final MediaType JSON = MediaType.get("application/json");
+ private static final String API_PATH = "/rest/api/3";
+
+ private final JiraAccount account;
+ private final HTTPUtils httpUtils;
+ private final Map headers;
+
+ public JiraValidation(JiraAccount account) {
+ this.account = account;
+ this.httpUtils = HTTPUtils.getInstance();
+ this.headers = Map.of(
+ "Authorization", Credentials.basic(account.getUsername(), account.getPassword()),
+ "Content-Type", "application/json",
+ "Accept", "application/json"
+ );
+ }
- private final JiraRestClient client;
-
- public JiraValidation(JiraRestClient client) {
- this.client = client;
+ private String apiUrl(String path) {
+ return account.getJiraUrl() + API_PATH + path;
}
/**
@@ -32,50 +46,109 @@ public JiraValidation(JiraRestClient client) {
*
* @param projectKey key of project where issue will be created
* @param issueSummary name of issue to be created
- * @return id of created issue
+ * @return key of created issue
*/
public String createIssue(String projectKey, String issueSummary) {
- final Optional bugTypeId = StreamSupport.stream(client.getMetadataClient().getIssueTypes().claim().spliterator(), false)
- .filter(t -> "bug".equalsIgnoreCase(t.getName())).findAny();
-
- if (bugTypeId.isEmpty()) {
- throw new RuntimeException("Unable to find bug type id");
+ JSONObject body = new JSONObject()
+ .put("fields", new JSONObject()
+ .put("project", new JSONObject().put("key", projectKey))
+ .put("summary", issueSummary)
+ .put("issuetype", new JSONObject().put("name", "Bug")));
+
+ HTTPUtils.Response response = httpUtils.post(
+ apiUrl("/issue"),
+ RequestBody.create(body.toString(), JSON),
+ headers
+ );
+
+ if (!response.isSuccessful()) {
+ throw new RuntimeException("Unable to create issue: " + response.getBody());
}
- final IssueInputBuilder builder = new IssueInputBuilder()
- .setProjectKey(projectKey)
- .setIssueTypeId(bugTypeId.get().getId())
- .setSummary(issueSummary);
- final String key = client.getIssueClient().createIssue(builder.build()).claim().getKey();
+ String key = new JSONObject(response.getBody()).getString("key");
LOG.debug("Created a new issue with key {}", key);
return key;
}
public void deleteIssue(String issueKey) {
LOG.debug("Deleting issue {}", issueKey);
- client.getIssueClient().deleteIssue(issueKey, true).claim();
+ httpUtils.delete(apiUrl("/issue/" + issueKey + "?deleteSubtasks=true"), headers);
}
public List getComments(String issueKey) {
LOG.debug("Getting comments of {}", issueKey);
- return StreamSupport.stream(client.getIssueClient().getIssue(issueKey).claim().getComments().spliterator(), false)
- .map(Comment::getBody).toList();
+ HTTPUtils.Response response = httpUtils.get(apiUrl("/issue/" + issueKey + "/comment"), headers);
+
+ if (!response.isSuccessful()) {
+ throw new RuntimeException("Unable to get comments for issue " + issueKey + ": " + response.getBody());
+ }
+
+ JSONArray comments = new JSONObject(response.getBody()).getJSONArray("comments");
+ List result = new ArrayList<>();
+ for (int i = 0; i < comments.length(); i++) {
+ JSONObject commentBody = comments.getJSONObject(i).optJSONObject("body");
+ if (commentBody != null) {
+ result.add(extractTextFromAdf(commentBody));
+ } else {
+ result.add(comments.getJSONObject(i).optString("body", ""));
+ }
+ }
+ return result;
}
public Issue getIssue(String issueKey) {
LOG.debug("Getting issue {}", issueKey);
- return convertToIssue(client.getIssueClient().getIssue(issueKey).claim());
+ HTTPUtils.Response response = httpUtils.get(apiUrl("/issue/" + issueKey), headers);
+
+ if (!response.isSuccessful()) {
+ throw new RuntimeException("Unable to get issue " + issueKey + ": " + response.getBody());
+ }
+
+ return convertToIssue(new JSONObject(response.getBody()));
}
public void addComment(String issueKey, String content) {
LOG.debug("Adding comment {} to issue {}", content, issueKey);
- final URI commentsUri = client.getIssueClient().getIssue(issueKey).claim().getCommentsUri();
- client.getIssueClient().addComment(commentsUri, Comment.valueOf(content)).claim();
+ JSONObject body = new JSONObject()
+ .put("body", new JSONObject()
+ .put("type", "doc")
+ .put("version", 1)
+ .put("content", new JSONArray()
+ .put(new JSONObject()
+ .put("type", "paragraph")
+ .put("content", new JSONArray()
+ .put(new JSONObject()
+ .put("type", "text")
+ .put("text", content))))));
+
+ HTTPUtils.Response response = httpUtils.post(
+ apiUrl("/issue/" + issueKey + "/comment"),
+ RequestBody.create(body.toString(), JSON),
+ headers
+ );
+
+ if (!response.isSuccessful()) {
+ throw new RuntimeException("Unable to add comment to issue " + issueKey + ": " + response.getBody());
+ }
}
public List getIssues(String jql) {
- return StreamSupport.stream(client.getSearchClient().searchJql(jql).claim().getIssues().spliterator(), false)
- .map(this::convertToIssue).toList();
+ HTTPUtils.Response response = httpUtils.post(
+ apiUrl("/search"),
+ RequestBody.create(new JSONObject().put("jql", jql).toString(), JSON),
+ headers
+ );
+
+ if (!response.isSuccessful()) {
+ throw new RuntimeException("Unable to search issues: " + response.getBody());
+ }
+
+ JSONArray issues = new JSONObject(response.getBody()).getJSONArray("issues");
+ List result = new ArrayList<>();
+ for (int i = 0; i < issues.length(); i++) {
+ result.add(convertToIssue(issues.getJSONObject(i)));
+ }
+ return result;
}
public List getIssues(String project, String customJQL) {
@@ -84,25 +157,65 @@ public List getIssues(String project, String customJQL) {
public void setTransition(String issueKey, int transitionId) {
LOG.debug("Transition issue {} - transition id: {}", issueKey, transitionId);
- final com.atlassian.jira.rest.client.api.domain.Issue issue = client.getIssueClient().getIssue(issueKey).claim();
- client.getIssueClient().transition(issue, new TransitionInput(transitionId));
+ JSONObject body = new JSONObject()
+ .put("transition", new JSONObject().put("id", String.valueOf(transitionId)));
+
+ HTTPUtils.Response response = httpUtils.post(
+ apiUrl("/issue/" + issueKey + "/transitions"),
+ RequestBody.create(body.toString(), JSON),
+ headers
+ );
+
+ if (!response.isSuccessful()) {
+ throw new RuntimeException("Unable to transition issue " + issueKey + ": " + response.getBody());
+ }
}
- private Issue convertToIssue(com.atlassian.jira.rest.client.api.domain.Issue issue) {
+ private Issue convertToIssue(JSONObject json) {
Issue result = new Issue();
- result.setKey(issue.getKey());
- result.setSummary(issue.getSummary());
- result.setDescription(issue.getDescription());
- result.setProjectKey(issue.getProject().getKey());
- result.setType(issue.getIssueType().getName());
- result.setPriority(issue.getPriority() == null ? "" : issue.getPriority().getName());
- result.setStatus(issue.getStatus().getName());
- final Iterable attachments = issue.getAttachments();
+ result.setKey(json.getString("key"));
+
+ JSONObject fields = json.getJSONObject("fields");
+ result.setSummary(fields.optString("summary", ""));
+ result.setDescription(fields.isNull("description") ? "" : extractTextFromAdf(fields.optJSONObject("description")));
+ result.setType(fields.optJSONObject("issuetype") != null ? fields.getJSONObject("issuetype").optString("name", "") : "");
+ result.setPriority(fields.optJSONObject("priority") != null ? fields.getJSONObject("priority").optString("name", "") : "");
+ result.setStatus(fields.optJSONObject("status") != null ? fields.getJSONObject("status").optString("name", "") : "");
+ result.setProjectKey(fields.optJSONObject("project") != null ? fields.getJSONObject("project").optString("key", "") : "");
+
+ JSONArray attachments = fields.optJSONArray("attachment");
if (attachments != null) {
- result.setAttachmentsIds(StreamSupport.stream(attachments.spliterator(), false).map(Attachment::getFilename).toList());
+ List attachmentNames = new ArrayList<>();
+ for (int i = 0; i < attachments.length(); i++) {
+ attachmentNames.add(attachments.getJSONObject(i).optString("filename", ""));
+ }
+ result.setAttachmentsIds(attachmentNames);
} else {
result.setAttachmentsIds(List.of());
}
+
return result;
}
+
+ private String extractTextFromAdf(JSONObject adf) {
+ if (adf == null) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder();
+ JSONArray content = adf.optJSONArray("content");
+ if (content != null) {
+ for (int i = 0; i < content.length(); i++) {
+ JSONObject node = content.getJSONObject(i);
+ if ("text".equals(node.optString("type"))) {
+ sb.append(node.optString("text", ""));
+ } else {
+ sb.append(extractTextFromAdf(node));
+ }
+ }
+ }
+ if ("text".equals(adf.optString("type"))) {
+ sb.append(adf.optString("text", ""));
+ }
+ return sb.toString();
+ }
}