diff --git a/src/main/java/com/pump/PumpApplication.java b/src/main/java/com/pump/PumpApplication.java index b85855e517255a38004dd21f122cc96ba1fe2810..9173ce063ffb52cffac3ef56e980fb7d27513c6a 100644 --- a/src/main/java/com/pump/PumpApplication.java +++ b/src/main/java/com/pump/PumpApplication.java @@ -29,7 +29,7 @@ public class PumpApplication { return _ -> { log.info("Starting application"); - gitPump.run(); + jiraPump.run(); log.info("Application ended"); }; diff --git a/src/main/java/com/pump/jira/JiraPump.java b/src/main/java/com/pump/jira/JiraPump.java index 89a8f3c72182df1a3784ee5f0d04c5110d6d7398..7499af4ffef0b8b90dc42063fd08f202740750be 100644 --- a/src/main/java/com/pump/jira/JiraPump.java +++ b/src/main/java/com/pump/jira/JiraPump.java @@ -8,12 +8,22 @@ import com.atlassian.jira.rest.client.api.domain.SearchResult; import com.atlassian.jira.rest.client.auth.AnonymousAuthenticationHandler; import com.atlassian.jira.rest.client.auth.BasicHttpAuthenticationHandler; import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory; +import com.pump.jira.service.AttachmentService; +import com.pump.jira.service.IssueService; +import com.pump.jira.service.ProjectService; +import com.pump.jira.service.UserService; +import com.pump.jira.service.VersionService; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.net.URI; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -21,6 +31,7 @@ import java.util.stream.StreamSupport; @Slf4j @Service +@RequiredArgsConstructor public class JiraPump { @Value("${jira.url}") @@ -41,19 +52,34 @@ public class JiraPump { @Value("${jira.timeout.seconds}") private int timeoutSeconds; + private final UserService userService; + private final VersionService versionService; + private final IssueService issueService; + private final ProjectService projectService; + private final AttachmentService attachmentService; + + /// public void run() { log.info("Starting JiraPump for project {}...", projectKey); JiraRestClient restClient = null; try { restClient = createJiraRestClient(); - Project project = fetchProjectDetails(restClient); - fetchProjectVersions(project); - fetchProjectComponents(project); - fetchProjectIssues(restClient); - fetchRecentActivity(restClient); - fetchAssignedIssues(restClient); - fetchCriticalIssues(restClient); + attachmentService.fetchAttachments(restClient); + +// TODO these methods will be moved into separate services +// Project project = fetchProjectDetails(restClient); +// fetchProjectVersions(project); +// fetchProjectComponents(project); +// fetchProjectIssues(restClient); +// fetchRecentActivity(restClient); +// fetchAssignedIssues(restClient); +// fetchCriticalIssues(restClient); +// fetchUserInformation(restClient); +// fetchIssueTypeStatistics(restClient); +// fetchWorkflowInformation(restClient); +// fetchProjectRoles(restClient); + fetchCustomFieldValues(restClient, "Story Points"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error("Interrupted while fetching data from Jira", e); @@ -198,4 +224,119 @@ public class JiraPump { } } } + + private void fetchUserInformation(JiraRestClient restClient) + throws InterruptedException, ExecutionException, TimeoutException { + log.info("Fetching active contributors in project"); + + String jqlActive = "project = " + projectKey + " ORDER BY updated DESC"; + SearchResult result = restClient.getSearchClient().searchJql(jqlActive, 50, 0, null) + .get(timeoutSeconds, TimeUnit.SECONDS); + + Set<String> usernames = new HashSet<>(); + result.getIssues().forEach(issue -> { + if (issue.getReporter() != null) { + usernames.add(issue.getReporter().getName()); + } + if (issue.getAssignee() != null) { + usernames.add(issue.getAssignee().getName()); + } + }); + + log.info("Found {} active contributors", usernames.size()); + for (String username : usernames) { + try { + com.atlassian.jira.rest.client.api.domain.User user = + restClient.getUserClient().getUser(username) + .get(timeoutSeconds, TimeUnit.SECONDS); + log.info(" User: {} ({}), Email: {}, Active: {}", + user.getDisplayName(), + user.getName(), + user.getEmailAddress(), + user.isActive()); + } catch (Exception e) { + log.warn("Could not fetch details for user: {}", username); + } + } + } + + private void fetchIssueTypeStatistics(JiraRestClient restClient) + throws InterruptedException, ExecutionException, TimeoutException { + log.info("Fetching issue type statistics"); + + Project project = restClient.getProjectClient().getProject(projectKey) + .get(timeoutSeconds, TimeUnit.SECONDS); + + project.getIssueTypes().forEach(issueType -> { + try { + String jql = "project = " + projectKey + " AND issuetype = \"" + issueType.getName() + "\""; + SearchResult result = restClient.getSearchClient().searchJql(jql, 0, 0, null) + .get(timeoutSeconds, TimeUnit.SECONDS); + + log.info(" {} - {} issues", issueType.getName(), result.getTotal()); + } catch (Exception e) { + log.warn("Error counting issues of type {}: {}", issueType.getName(), e.getMessage()); + } + }); + } + + private void fetchWorkflowInformation(JiraRestClient restClient) + throws InterruptedException, ExecutionException, TimeoutException { + log.info("Fetching workflow status information"); + + String jql = "project = " + projectKey + " ORDER BY created DESC"; + SearchResult result = restClient.getSearchClient().searchJql(jql, 1, 0, null) + .get(timeoutSeconds, TimeUnit.SECONDS); + + if (result.getTotal() > 0) { + Issue sampleIssue = result.getIssues().iterator().next(); + + log.info("Status categories in workflow:"); + Map<String, Integer> statusCounts = new HashMap<>(); + + // Now get all issues and count by status + String jqlAll = "project = " + projectKey; + SearchResult allResult = restClient.getSearchClient().searchJql(jqlAll, maxIssues, 0, null) + .get(timeoutSeconds, TimeUnit.SECONDS); + + allResult.getIssues().forEach(issue -> { + String status = issue.getStatus().getName(); + statusCounts.put(status, statusCounts.getOrDefault(status, 0) + 1); + }); + + statusCounts.forEach((status, count) -> + log.info(" {} - {} issues", status, count) + ); + } + } + + private void fetchProjectRoles(JiraRestClient restClient) + throws InterruptedException, ExecutionException, TimeoutException { + log.info("Fetching project roles"); + + final String roleEndpoint = jiraApiUrl + "/rest/api/2/project/" + projectKey + "/role"; + + log.info("Note: Project role details require custom REST API implementation"); + } + + private void fetchCustomFieldValues(JiraRestClient restClient, String customFieldName) + throws InterruptedException, ExecutionException, TimeoutException { + log.info("Fetching values for custom field: {}", customFieldName); + + String jql = "project = " + projectKey; + SearchResult result = restClient.getSearchClient().searchJql(jql, maxIssues, 0, null) + .get(timeoutSeconds, TimeUnit.SECONDS); + + result.getIssues().forEach(issue -> { + issue.getFields().forEach(field -> { + if (field.getName().equals(customFieldName)) { + log.info(" [{}] {} - {}: {}", + issue.getKey(), + issue.getSummary(), + customFieldName, + field.getValue() != null ? field.getValue().toString() : "null"); + } + }); + }); + } } \ No newline at end of file diff --git a/src/main/java/com/pump/jira/service/AttachmentService.java b/src/main/java/com/pump/jira/service/AttachmentService.java new file mode 100644 index 0000000000000000000000000000000000000000..691bfd92842f90e461771a2d01e4b91becee85cd --- /dev/null +++ b/src/main/java/com/pump/jira/service/AttachmentService.java @@ -0,0 +1,90 @@ +package com.pump.jira.service; + + +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.Issue; +import com.atlassian.jira.rest.client.api.domain.SearchResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.StreamSupport; + +@Slf4j +@Service +public class AttachmentService { + + @Value("${jira.project.key}") + private String projectKey; + + @Value("${jira.max.issues:100}") + private int maxIssues; + + @Value("${jira.timeout.seconds:30}") + private int timeoutSeconds; + + /// id -> Artifact.externalId -> TODO NOT EXISTING + /// fileName -> Artifact.name + /// properties -> Artifact.description + /// created -> Artifact.created + /// authorObject -> Artifact.author + /// "File" -> Artifact.artifactClass + /// mimeType -> Artifact.mimeType + /// fileSize -> Artifact.size + public void fetchAttachments(JiraRestClient restClient) + throws InterruptedException, ExecutionException, TimeoutException { + log.info("Fetching attachments for project: {}", projectKey); + + // First find issues with attachments + String jql = "project = " + projectKey + " AND attachments IS NOT EMPTY ORDER BY updated DESC"; + SearchResult result = restClient.getSearchClient().searchJql(jql, maxIssues, 0, null) + .get(timeoutSeconds, TimeUnit.SECONDS); + + log.info("Found {} issues with attachments", result.getTotal()); + int totalAttachments = 0; + + // For each issue in the search results, fetch the complete issue with all fields + for (Issue basicIssue : result.getIssues()) { + + // Get the full issue with all fields including attachments + Issue completeIssue = restClient.getIssueClient().getIssue(basicIssue.getKey()) + .get(timeoutSeconds, TimeUnit.SECONDS); + + Iterable<Attachment> attachments = completeIssue.getAttachments(); + + if (attachments != null) { + int count = (int) StreamSupport.stream(attachments.spliterator(), false).count(); + + if (count > 0) { + log.info("Issue [{}] - {} has {} attachment(s)", + completeIssue.getKey(), completeIssue.getSummary(), count); + + // Data that we want to save + for (Attachment attachment : attachments) { + log.info("ID: {}, Filename: {}, Properties: {}, Created: {}, AuthorObject: {}, " + + "\"File\": {}, MimeType: {}, FileSize: {}", + 0, // TODO WE HAVE NOT ID + attachment.getFilename(), + "NO PROPERTIES", // TODO we have no properties + attachment.getCreationDate(), + attachment.getAuthor().getDisplayName(), + attachment.getSelf(), // TODO we have no file yet + // TODO it's request to API so we will most likely have to fetch it additionally + attachment.getMimeType(), + attachment.getSize() + ); + totalAttachments++; + } + } + } else { + log.debug("Issue [{}] reports having attachments but returned null", basicIssue.getKey()); + } + } + + log.info("Total attachments found: {}", totalAttachments); + } +} diff --git a/src/main/java/com/pump/jira/service/IssueService.java b/src/main/java/com/pump/jira/service/IssueService.java new file mode 100644 index 0000000000000000000000000000000000000000..3f4829327ab7f7f53ee456716ee4056f567aa3e7 --- /dev/null +++ b/src/main/java/com/pump/jira/service/IssueService.java @@ -0,0 +1,10 @@ +package com.pump.jira.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class IssueService { + +} diff --git a/src/main/java/com/pump/jira/service/ProjectService.java b/src/main/java/com/pump/jira/service/ProjectService.java new file mode 100644 index 0000000000000000000000000000000000000000..474af4c0f1ad7319907969b2733666273e867b87 --- /dev/null +++ b/src/main/java/com/pump/jira/service/ProjectService.java @@ -0,0 +1,10 @@ +package com.pump.jira.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class ProjectService { + +} diff --git a/src/main/java/com/pump/jira/service/UserService.java b/src/main/java/com/pump/jira/service/UserService.java new file mode 100644 index 0000000000000000000000000000000000000000..5055c922a2d400c5d55dfcd25970f79ef7d44a22 --- /dev/null +++ b/src/main/java/com/pump/jira/service/UserService.java @@ -0,0 +1,13 @@ +package com.pump.jira.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class UserService { + + public void run() { + log.info("Running user service"); + } +} diff --git a/src/main/java/com/pump/jira/service/VersionService.java b/src/main/java/com/pump/jira/service/VersionService.java new file mode 100644 index 0000000000000000000000000000000000000000..de0d30ab35a4aecef6ab3cd4f1a2f9544cede629 --- /dev/null +++ b/src/main/java/com/pump/jira/service/VersionService.java @@ -0,0 +1,10 @@ +package com.pump.jira.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class VersionService { + +}