Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions jira/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,11 @@
<artifactId>htmlunit</artifactId>
<version>3.9.0</version>
</dependency>
<dependency>
<groupId>com.konghq</groupId>
<artifactId>unirest-java</artifactId>
<version>3.14.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.atlassian.sal/sal-api -->
<dependency>
<groupId>com.atlassian.sal</groupId>
Expand Down Expand Up @@ -442,6 +447,10 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-core</artifactId>
</dependency>
</dependencies>
<repositories>
<repository>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,15 @@ public final class JiraConstants {
public static final String FALSE = "False";
public static final String START_AT_ATTRIBUTE = "startAt";
public static final String MAX_RESULTS_ATTRIBUTE = "maxResults";
public static final String FIELDS_BY_KEYS_ATTRIBUTE = "fieldsByKeys";
public static final int MAX_JQL_LENGTH_FOR_HTTP_GET = 3000;
public static final String JQL_ATTRIBUTE = "jql";
public static final String FILTER_FAVOURITE_PATH = "filter/favourite";
public static final String FILTER_PATH_FORMAT = "filter/%s";
public static final String SEARCH_URI_PREFIX = "search";
public static final String EXPAND_ATTRIBUTE = "expand";
public static final String FIELDS_ATTRIBUTE = "fields";
public static final String NEXT_PAGE_TOKEN_ATTRIBUTE = "nextPageToken";
public static final String DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
public static final String AGGREGATED_TIME_SPENT = "aggregatetimespent";
public static final String AGGREGATED_TIME_ORIGINAL = "aggregatetimeoriginalestimate";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.publicissapient.kpidashboard.jira.exception;

public class JiraApiException extends Exception {
public JiraApiException(String message) {
super(message);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.publicissapient.kpidashboard.jira.model;

import com.atlassian.jira.rest.client.api.domain.Issue;
import lombok.Data;

@Data
public class JiraSearchResponse {
private final Iterable<Issue> issues;
private boolean isLast;
private String nextPageToken;

public JiraSearchResponse(Iterable<Issue> issues, boolean isLast, String nextPageToken) {
this.issues = issues;
this.isLast = isLast;
this.nextPageToken = nextPageToken;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.publicissapient.kpidashboard.jira.parser;

import com.atlassian.jira.rest.client.api.domain.Issue;
import com.publicissapient.kpidashboard.jira.model.JiraSearchResponse;
import com.atlassian.jira.rest.client.internal.json.GenericJsonArrayParser;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;

import java.util.Collections;

public class JiraSearchResponseParser {

public JiraSearchResponse parse(JSONObject json) throws JSONException {
JSONArray issuesJsonArray = json.getJSONArray("issues");

Iterable<Issue> issues;
if (issuesJsonArray.length() > 0) {
CustomIssueJsonParser issueParser = new CustomIssueJsonParser(
json.optJSONObject("names"), json.optJSONObject("schema"));
GenericJsonArrayParser<Issue> issuesParser = GenericJsonArrayParser.create(issueParser);
issues = issuesParser.parse(issuesJsonArray);
} else {
issues = Collections.emptyList();
}

boolean isLast = json.optBoolean("isLast", true);
String nextPageToken = json.optString("nextPageToken", null);

return new JiraSearchResponse(issues, isLast, nextPageToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,32 @@
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import com.atlassian.jira.rest.client.api.IssueRestClient;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.publicissapient.kpidashboard.jira.constant.JiraConstants;
import com.publicissapient.kpidashboard.jira.exception.JiraApiException;
import com.publicissapient.kpidashboard.jira.model.JiraSearchResponse;
import com.publicissapient.kpidashboard.jira.parser.JiraSearchResponseParser;
import kong.unirest.HttpResponse;
import kong.unirest.JsonNode;
import kong.unirest.Unirest;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.jettison.json.JSONException;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
Expand All @@ -48,6 +67,12 @@
import io.atlassian.util.concurrent.Promise;
import lombok.extern.slf4j.Slf4j;

import javax.annotation.Nullable;

import static com.atlassian.jira.rest.client.api.IssueRestClient.Expandos.CHANGELOG;
import static com.atlassian.jira.rest.client.api.IssueRestClient.Expandos.NAMES;
import static com.atlassian.jira.rest.client.api.IssueRestClient.Expandos.SCHEMA;

@Slf4j
@Service
public class FetchEpicDataImpl implements FetchEpicData {
Expand All @@ -57,6 +82,12 @@ public class FetchEpicDataImpl implements FetchEpicData {
private JiraCommonService jiraCommonService;
@Autowired
private JiraProcessorConfig jiraProcessorConfig;
private static final Function<IssueRestClient.Expandos, String> EXPANDO_TO_PARAM = from -> from.name().toLowerCase(); // NOSONAR
private static final String JQL_SEARCH_URL = "rest/api/latest/search/jql";
private static final String ACCEPT = "accept";
private static final String APPLICATION_JSON = "application/json";
private static final String CONTENT_TYPE = "Content-Type";
private static final int PAGE_SIZE = 50;

@Override
public List<Issue> fetchEpic(ProjectConfFieldMapping projectConfig, String boardId, ProcessorJiraRestClient client,
Expand Down Expand Up @@ -84,7 +115,7 @@ public List<Issue> fetchEpic(ProjectConfFieldMapping projectConfig, String board
throw mfe;
}

return getEpicIssuesQuery(epicList, client);
return getEpicIssuesViaAdvancedJql(epicList, projectConfig.getJira());
}

private List<Issue> getEpicIssuesQuery(List<String> epicKeyList, ProcessorJiraRestClient client)
Expand Down Expand Up @@ -131,6 +162,157 @@ private List<Issue> getEpicIssuesQuery(List<String> epicKeyList, ProcessorJiraRe
return issueList;
}

public List<Issue> getEpicIssuesViaAdvancedJql(List<String> epicKeyList,JiraToolConfig jiraToolConfig) {
List<Issue> allIssues = new ArrayList<>();

if (epicKeyList == null || epicKeyList.isEmpty()) {
return allIssues;
}

try {
String jql = "key in (" + String.join(",", epicKeyList) + ")";

String nextPageToken = null;
boolean isLast;

do {

Set<String> fields = new HashSet<>();
fields.add("*all");

JiraSearchResponse result = searchJql(jql,PAGE_SIZE, fields, nextPageToken, jiraToolConfig);

for (Issue issue : result.getIssues()) {
allIssues.add(issue);
}

isLast = result.isLast();
nextPageToken = result.getNextPageToken();

} while (!isLast && nextPageToken != null);

} catch (JiraApiException | JSONException e) {
log.error("Error while fetching Epic issues", e.getCause());
}

return allIssues;
}

public JiraSearchResponse searchJql(@Nullable String jql, @Nullable Integer maxResults,
@Nullable Set<String> fields, String nextPageToken, JiraToolConfig jiraToolConfig) throws JSONException, JiraApiException {
final Iterable<String> expandosValues = Iterables.transform(java.util.List.of(SCHEMA, NAMES, CHANGELOG),
EXPANDO_TO_PARAM);
final String notNullJql = StringUtils.defaultString(jql);
if (notNullJql.length() > (JiraConstants.MAX_JQL_LENGTH_FOR_HTTP_GET)) {
return advancedJqlSearchPost(maxResults, expandosValues, notNullJql, fields, nextPageToken, jiraToolConfig);
} else {
return advancedJqlSearchGet(maxResults, expandosValues, notNullJql, fields, nextPageToken, jiraToolConfig);
}
}
private JiraSearchResponse advancedJqlSearchGet(
@Nullable Integer maxResults,
Iterable<String> expandosValues,
String jql,
@Nullable Set<String> fields,
String nextPageToken,
JiraToolConfig jiraToolConfig) throws JSONException, JiraApiException {

Connection connection = jiraToolConfig.getConnection()
.orElseThrow(() -> new JiraApiException("No connection available in JiraToolConfig"));

String password = connection.isBearerToken()
? jiraCommonService.decryptJiraPassword(connection.getPatOAuthToken())
: jiraCommonService.decryptJiraPassword(connection.getPassword());

String expandJoined = (expandosValues != null)
? StreamSupport.stream(expandosValues.spliterator(), false)
.collect(Collectors.joining(","))
: null;

String fieldsJoined = (fields != null && !fields.isEmpty())
? String.join(",", fields)
: null;

HttpResponse<JsonNode> response = Unirest.get(connection.getBaseUrl() + JQL_SEARCH_URL)
.basicAuth(connection.getUsername(), password)
.header(ACCEPT, APPLICATION_JSON)
.queryString(JiraConstants.JQL_ATTRIBUTE, jql)
.queryString(JiraConstants.FIELDS_BY_KEYS_ATTRIBUTE, true)
.queryString(JiraConstants.MAX_RESULTS_ATTRIBUTE, maxResults)
.queryString(JiraConstants.NEXT_PAGE_TOKEN_ATTRIBUTE, nextPageToken)
.queryString(JiraConstants.EXPAND_ATTRIBUTE, expandJoined)
.queryString(JiraConstants.FIELDS_ATTRIBUTE, fieldsJoined)
.asJson();

if (response.getStatus() != 200) {
throw new JiraApiException("Failed to fetch issues: HTTP " + response.getStatus());
}

kong.unirest.json.JSONObject jsonFromUnirest = response.getBody().getObject();
org.codehaus.jettison.json.JSONObject jsonObject =
new org.codehaus.jettison.json.JSONObject(jsonFromUnirest.toString());

return new JiraSearchResponseParser().parse(jsonObject);
}

private JiraSearchResponse advancedJqlSearchPost(
@Nullable Integer maxResults,
Iterable<String> expandosValues,
String jql,
@Nullable Set<String> fields,
String nextPageToken,
JiraToolConfig jiraToolConfig) throws JSONException, JiraApiException {

Connection connection = jiraToolConfig.getConnection()
.orElseThrow(() -> new JiraApiException("No connection available in JiraToolConfig"));

String password = connection.isBearerToken()
? jiraCommonService.decryptJiraPassword(connection.getPatOAuthToken())
: jiraCommonService.decryptJiraPassword(connection.getPassword());

ObjectNode payload = JsonNodeFactory.instance.objectNode();

String expandJoined = (expandosValues != null)
? StreamSupport.stream(expandosValues.spliterator(), false).collect(Collectors.joining(","))
: null;
if (expandJoined != null) {
payload.put(JiraConstants.EXPAND_ATTRIBUTE, expandJoined);
}

ArrayNode fieldsArray = payload.putArray(JiraConstants.FIELDS_ATTRIBUTE);
if (fields != null && !fields.isEmpty()) {
for (String field : fields) {
fieldsArray.add(field);
}
}

payload.put(JiraConstants.FIELDS_BY_KEYS_ATTRIBUTE, true);
payload.put(JiraConstants.JQL_ATTRIBUTE, jql);
if (maxResults != null) {
payload.put(JiraConstants.MAX_RESULTS_ATTRIBUTE, maxResults);
}
if (nextPageToken != null) {
payload.put(JiraConstants.NEXT_PAGE_TOKEN_ATTRIBUTE, nextPageToken);
}

HttpResponse<JsonNode> response = Unirest.post(connection.getBaseUrl() + JQL_SEARCH_URL)
.basicAuth(connection.getUsername(), password)
.header(ACCEPT, APPLICATION_JSON)
.header(CONTENT_TYPE, APPLICATION_JSON)
.body(payload.toString())
.asJson();

if (response.getStatus() != 200) {
throw new JiraApiException("Failed to fetch issues: HTTP " + response.getStatus());
}

kong.unirest.json.JSONObject jsonFromUnirest = response.getBody().getObject();
org.codehaus.jettison.json.JSONObject jsonObject =
new org.codehaus.jettison.json.JSONObject(jsonFromUnirest.toString());

return new JiraSearchResponseParser().parse(jsonObject);
}

private boolean populateData(String sprintReportObj, List<String> epicList) {
boolean isLast = true;
if (StringUtils.isNotBlank(sprintReportObj)) {
Expand Down
Loading