From 520bf6ac8eced617fd89b0a22f09089350e01f1b Mon Sep 17 00:00:00 2001 From: Marco Carletti Date: Thu, 19 Mar 2026 16:50:01 +0100 Subject: [PATCH] RHBAC-320: replaces Jira client with REST client --- system-x/services/jira/README.md | 21 ++ system-x/services/jira/pom.xml | 53 ----- .../java/software/tnb/jira/service/Jira.java | 27 +-- .../tnb/jira/validation/JiraValidation.java | 203 ++++++++++++++---- 4 files changed, 182 insertions(+), 122 deletions(-) create mode 100644 system-x/services/jira/README.md 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(); + } }