diff --git a/src/main/java/com/pump/jira/JiraPump.java b/src/main/java/com/pump/jira/JiraPump.java index 2f5120970b1d4fe7a676155dc31784aa83feb970..885dfebd8947dcc4bdaab6af7f54301d1616060c 100644 --- a/src/main/java/com/pump/jira/JiraPump.java +++ b/src/main/java/com/pump/jira/JiraPump.java @@ -1,15 +1,12 @@ package com.pump.jira; import com.atlassian.jira.rest.client.api.JiraRestClient; -import com.atlassian.jira.rest.client.api.SearchRestClient; -import com.atlassian.jira.rest.client.api.domain.Issue; -import com.atlassian.jira.rest.client.api.domain.Project; -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.LabelService; import com.pump.jira.service.ProjectService; import com.pump.jira.service.UserService; import com.pump.jira.service.VersionService; @@ -19,15 +16,6 @@ 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; -import java.util.stream.StreamSupport; @Slf4j @Service @@ -46,131 +34,63 @@ public class JiraPump { @Value("${jira.project.key}") private String projectKey; - @Value("${jira.max.issues}") - private int maxIssues; - - @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; + private final LabelService labelService; + // TODO we need to prepare following "objects" (name of SPADe tables) - // ToolProjectInstance (Project) + // Artifact ✅ -> attachmentService.fetchAttachments // Configuration - // DevelopmentProgram - // Identity - // IdentityGroup - // Iteration - // Phase - // Project - // Role + // DevelopmentProgram ✅ -> fetchProjectCategory + // Identity ✅ -> userService.fetchApplicationUsers + // IdentityGroup ✅ -> userService.fetchGroupBean(restClient) + // Iteration ✅ -> versionService.fetchIterations + // Phase ✅ -> versionService.fetchVersion + // Project ??? + // Role ✅ -> userService.fetchProjectRoleActors // Release - // ToolProjectInstance - // WorkItemChange + // ToolProjectInstance ✅ -> projectService.fetchToolProjectInstances + // WorkItemChange ✅ -> issueService.fetchChanges // WorkUnit - // WorkUnitCategory - // WorkUnitPriority + // WorkUnitCategory ✅ -> issueService.fetchProjectComponents + // WorkUnitPriority ✅ -> issueService.fetchWorkUnitPriority // WorkUnitRelation - // WorkUnitSeverity - // WorkUnitStatus - // WorkUnitType - // any + // WorkUnitSeverity ❌ -> issueService.fetchIssues + // WorkUnitStatus ✅-> issueService.fetchStatusAndResolution + // WorkUnitType ✅ -> issueService.fetchAllIssueTypes + // any ✅ -> labelService.fetchLabels public void run() { log.info("Starting JiraPump for project {}...", projectKey); JiraRestClient restClient = null; try { restClient = createJiraRestClient(); - attachmentService.fetchAttachments(restClient); - userService.fetchApplicationUsers(restClient); - versionService.fetchIterations(restClient); - projectService.fetchToolProjectInstances(restClient); - issueService.fetchIssues(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); - } catch (ExecutionException | TimeoutException e) { - log.error("Error fetching data from Jira", e); - } catch (Exception e) { +// attachmentService.fetchAttachments(restClient); +// userService.fetchApplicationUsers(restClient); +// versionService.fetchIterations(restClient); +// projectService.fetchToolProjectInstances(restClient); +// issueService.fetchIssues(restClient); +// issueService.fetchAllIssueTypes(restClient); +// labelService.fetchLabels(restClient); +// issueService.fetchProjectComponents(restClient); +// userService.fetchProjectRoleActors(restClient); +// userService.fetchGroupBean(restClient); +// issueService.fetchStatusAndResolution(restClient); +// projectService.fetchProjectCategory(restClient); +// issueService.fetchWorkUnitPriority(restClient); +// versionService.fetchVersion(restClient); +// issueService.fetchChanges(restClient); + } catch (Exception e) { log.error("Unexpected error while working with Jira", e); } finally { closeClient(restClient); } } - - private void fetchProjectIssues(JiraRestClient restClient) throws InterruptedException, ExecutionException, TimeoutException { - log.info("Fetching issues for project: {}", projectKey); - - // Query open issues - String jqlOpen = "project = " + projectKey + " AND status = Open ORDER BY created DESC"; - fetchIssuesByJql(restClient, jqlOpen, "Open issues"); - - // Query recently resolved issues - String jqlResolved = "project = " + projectKey + " AND status = Resolved ORDER BY resolutiondate DESC"; - fetchIssuesByJql(restClient, jqlResolved, "Recently resolved issues"); - } - - private void fetchIssuesByJql(JiraRestClient restClient, String jql, String category) - throws InterruptedException, ExecutionException, TimeoutException { - log.info("Executing search: {}", jql); - - SearchRestClient searchClient = restClient.getSearchClient(); - SearchResult result = searchClient.searchJql(jql, maxIssues, 0, null) - .get(timeoutSeconds, TimeUnit.SECONDS); - - log.info("Found {} {} (displaying up to {})", result.getTotal(), category, maxIssues); - - List<Issue> issues = StreamSupport.stream(result.getIssues().spliterator(), false) - .toList(); - - for (Issue issue : issues) { - log.info(" [{}] {} - {} (Reporter: {}, Status: {})", - issue.getKey(), - issue.getSummary(), - issue.getCreationDate().toString(), - issue.getReporter() != null ? issue.getReporter().getDisplayName() : "Unknown", - issue.getStatus().getName() - ); - } - } - - private void fetchRecentActivity(JiraRestClient restClient) throws InterruptedException, ExecutionException, TimeoutException { - log.info("Fetching recent activity"); - - String jqlRecent = "project = " + projectKey + " ORDER BY updated DESC"; - SearchResult result = restClient.getSearchClient().searchJql(jqlRecent, 10, 0, null) - .get(timeoutSeconds, TimeUnit.SECONDS); - - log.info("Recent activity:"); - - result.getIssues().forEach(issue -> - log.info(" [{}] {} - Updated: {}", - issue.getKey(), - issue.getSummary(), - issue.getUpdateDate().toString() - ) - ); - } - private JiraRestClient createJiraRestClient() { log.info("Establishing connection to Jira at {}", jiraApiUrl); AsynchronousJiraRestClientFactory factory = new AsynchronousJiraRestClientFactory(); @@ -191,53 +111,6 @@ public class JiraPump { } } - private Project fetchProjectDetails(JiraRestClient restClient) - throws InterruptedException, ExecutionException, TimeoutException { - log.info("Fetching project: {}", projectKey); - Project project = restClient.getProjectClient() - .getProject(projectKey) - .get(timeoutSeconds, TimeUnit.SECONDS); - - log.info("Project: {} ({})", project.getName(), project.getKey()); - log.info("Description: {}", project.getDescription()); - log.info("Lead: {}", project.getLead().getDisplayName()); - log.info("URI: {}", project.getUri()); - - return project; - } - - private void fetchProjectVersions(Project project) { - log.info("Project versions:"); - project.getVersions().forEach(version -> - log.info(" {} (released: {}, archived: {})", - version.getName(), - version.isReleased(), - version.isArchived() - ) - ); - } - - private void fetchProjectComponents(Project project) { - log.info("Project components:"); - project.getComponents().forEach(component -> - log.info(" {}", component.getName()) - ); - } - - private void fetchCriticalIssues(JiraRestClient restClient) - throws InterruptedException, ExecutionException, TimeoutException { - String jqlCritical = "project = " + projectKey + " AND priority in (Blocker, Critical) ORDER BY created DESC"; - fetchIssuesByJql(restClient, jqlCritical, "Critical issues"); - } - - private void fetchAssignedIssues(JiraRestClient restClient) - throws InterruptedException, ExecutionException, TimeoutException { - if (username != null && !username.isEmpty()) { - String jqlAssigned = "project = " + projectKey + " AND assignee = currentUser() ORDER BY updated DESC"; - fetchIssuesByJql(restClient, jqlAssigned, "Issues assigned to you"); - } - } - private void closeClient(JiraRestClient restClient) { if (restClient != null) { try { @@ -248,119 +121,4 @@ 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 index 691bfd92842f90e461771a2d01e4b91becee85cd..39b37058f1318fbbaebadefb8c9fbfc20a48602e 100644 --- a/src/main/java/com/pump/jira/service/AttachmentService.java +++ b/src/main/java/com/pump/jira/service/AttachmentService.java @@ -21,10 +21,10 @@ public class AttachmentService { @Value("${jira.project.key}") private String projectKey; - @Value("${jira.max.issues:100}") + @Value("${jira.max.issues}") private int maxIssues; - @Value("${jira.timeout.seconds:30}") + @Value("${jira.timeout.seconds}") private int timeoutSeconds; /// id -> Artifact.externalId -> TODO NOT EXISTING diff --git a/src/main/java/com/pump/jira/service/IssueService.java b/src/main/java/com/pump/jira/service/IssueService.java index 7a98daad3364d1ae4d14284548216b58b4b60d4f..3922116fa53aee2fabb41399801ade6d59134094 100644 --- a/src/main/java/com/pump/jira/service/IssueService.java +++ b/src/main/java/com/pump/jira/service/IssueService.java @@ -7,6 +7,7 @@ import org.joda.time.DateTime; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import java.net.URI; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -180,21 +181,465 @@ public class IssueService { return dateTime != null ? dateTime.toString("yyyy-MM-dd HH:mm:ss") : "N/A"; } + + /// id -> WorkUnitType.externalId + /// name -> WorkUnitType.name + /// description + subtask -> WorkUnitType.description + public void fetchAllIssueTypes(JiraRestClient restClient) + throws InterruptedException, ExecutionException, TimeoutException { + log.info("Fetching all available issue types"); + + try { + // Get project to access available issue types + Project project = restClient.getProjectClient() + .getProject(projectKey) + .get(timeoutSeconds, TimeUnit.SECONDS); + + log.info("Available Issue Types for project {}:", projectKey); + + for (IssueType issueType : project.getIssueTypes()) { + log.info("------------------------------------"); + log.info(" ID: {}", issueType.getId()); + log.info(" Name: {}", issueType.getName()); + + if (issueType.getDescription() != null && !issueType.getDescription().isEmpty()) { + log.info(" Description: {}", issueType.getDescription()); + } else { + log.info(" Description: None"); + } + + log.info(" Is Subtask: {}", issueType.isSubtask()); + + // Count issues of this type + String jql = "project = " + projectKey + " AND issuetype = " + issueType.getId(); + SearchResult searchResult = restClient.getSearchClient() + .searchJql(jql, 1, 0, null) + .get(timeoutSeconds, TimeUnit.SECONDS); + + log.info(" Number of issues: {}", searchResult.getTotal()); + } + + log.info("------------------------------------"); + log.info("Total issue types: {}", project.getIssueTypes().toString()); + } catch (Exception e) { + log.error("Error fetching issue types: {}", e.getMessage(), e); + } + } + + /// id -> WorkUnitCategory.externalId + /// name -> WorkUnitCategory.name + /// description -> WorkUnitCategory.description + public void fetchProjectComponents(JiraRestClient restClient) + throws InterruptedException, ExecutionException, TimeoutException { + log.info("Fetching project components for project {}", projectKey); + + try { + // Get detailed project information with components + Project project = restClient.getProjectClient() + .getProject(projectKey) + .get(timeoutSeconds, TimeUnit.SECONDS); + + Iterable<BasicComponent> components = project.getComponents(); + boolean hasComponents = components.iterator().hasNext(); + + if (hasComponents) { + log.info("Found components for project {}:", projectKey); + + for (BasicComponent component : components) { + log.info("------------------------------------"); + log.info("ID: {}", component.getId()); + log.info("Name: {}", component.getName()); + + // Get detailed component information + Component detailedComponent = restClient.getComponentClient() + .getComponent(component.getSelf()) + .get(timeoutSeconds, TimeUnit.SECONDS); + + if (detailedComponent.getDescription() != null && !detailedComponent.getDescription().isEmpty()) { + log.info("Description: {}", detailedComponent.getDescription()); + } else { + log.info("Description: None"); + } + + if (detailedComponent.getLead() != null) { + log.info("Lead: {} ({})", + detailedComponent.getLead().getDisplayName(), + detailedComponent.getLead().getName()); + } + + // Count issues using this component + String jql = "project = " + projectKey + " AND component = \"" + component.getName() + "\""; + SearchResult searchResult = restClient.getSearchClient() + .searchJql(jql, 1, 0, null) + .get(timeoutSeconds, TimeUnit.SECONDS); + + log.info("Number of issues: {}", searchResult.getTotal()); + } + + log.info("------------------------------------"); + log.info("Total components: {}", countItems(components)); + } else { + log.info("No components found for project {}", projectKey); + } + } catch (Exception e) { + log.error("Error fetching project components: {}", e.getMessage(), e); + } + } + /** - * Formats bytes to a human-readable size + * Helper method to count items in an Iterable */ - private String formatBytes(Long bytes) { - if (bytes == null) return "Unknown"; + private int countItems(Iterable<?> iterable) { + int count = 0; + for (Object item : iterable) { + count++; + } + return count; + } + + /// Fetching two types and mapping into same table + /// + /// 1. Status + /// id -> WorkUnitStatus.externalId + /// name -> WorkUnitStatus.name + /// description -> WorkUnitStatus.description + /// + /// 2. Resolution + /// id -> WorkUnitResolution.externalId + /// name -> WorkUnitResolution.name + /// description -> WorkUnitResolution.description + public void fetchStatusAndResolution(JiraRestClient restClient) + throws InterruptedException, ExecutionException, TimeoutException { + log.info("Fetching statuses and resolutions for project {}", projectKey); + + try { + // Collect statuses from project issues + log.info("Fetching statuses from project issues..."); + String jql = "project = " + projectKey; + SearchResult searchResult = restClient.getSearchClient() + .searchJql(jql, maxIssues, 0, null) + .get(timeoutSeconds, TimeUnit.SECONDS); + + // Track unique statuses and resolutions + java.util.Map<String, Status> statuses = new java.util.HashMap<>(); + java.util.Map<String, Resolution> resolutions = new java.util.HashMap<>(); + + // Extract status and resolution from each issue + for (Issue issue : searchResult.getIssues()) { + if (issue.getStatus() != null) { + Status status = issue.getStatus(); + statuses.putIfAbsent(status.getId().toString(), status); + } + + if (issue.getResolution() != null) { + Resolution resolution = issue.getResolution(); + resolutions.putIfAbsent(resolution.getId().toString(), resolution); + } + } + + // Log statuses information + log.info("------------------------------------"); + log.info("Available Statuses:"); + for (Status status : statuses.values()) { + log.info("------------------------------------"); + log.info("Status ID: {}", status.getId()); + log.info("Status Name: {}", status.getName()); + + if (status.getDescription() != null) { + log.info("Status Description: {}", status.getDescription()); + } else { + log.info("Status Description: None"); + } + + if (status.getIconUrl() != null) { + log.info("Status Icon URL: {}", status.getIconUrl()); + } + + // Count issues with this status + String statusJql = "project = " + projectKey + " AND status = \"" + status.getName() + "\""; + SearchResult statusResult = restClient.getSearchClient() + .searchJql(statusJql, 1, 0, null) + .get(timeoutSeconds, TimeUnit.SECONDS); + log.info("Number of issues: {}", statusResult.getTotal()); + } + + log.info("------------------------------------"); + log.info("Total statuses: {}", statuses.size()); + + // Log resolutions information + log.info("------------------------------------"); + log.info("Available Resolutions:"); + for (Resolution resolution : resolutions.values()) { + log.info("------------------------------------"); + log.info("Resolution ID: {}", resolution.getId()); + log.info("Resolution Name: {}", resolution.getName()); + + if (resolution.getDescription() != null) { + log.info("Resolution Description: {}", resolution.getDescription()); + } else { + log.info("Resolution Description: None"); + } + + // Count issues with this resolution + String resolutionJql = "project = " + projectKey + " AND resolution = \"" + resolution.getName() + "\""; + SearchResult resolutionResult = restClient.getSearchClient() + .searchJql(resolutionJql, 1, 0, null) + .get(timeoutSeconds, TimeUnit.SECONDS); + log.info("Number of issues: {}", resolutionResult.getTotal()); + } - final String[] units = new String[] { "B", "KB", "MB", "GB", "TB" }; - int unitIndex = 0; - double size = bytes; + log.info("------------------------------------"); + log.info("Total resolutions: {}", resolutions.size()); - while (size > 1024 && unitIndex < units.length - 1) { - size /= 1024; - unitIndex++; + } catch (Exception e) { + log.error("Error fetching status and resolution information: {}", e.getMessage(), e); } + } - return String.format("%.2f %s", size, units[unitIndex]); + /// id -> WorkUnitPriority.externalId + /// name -> WorkUnitPriority.name + /// description -> WorkUnitPriority.description + public void fetchWorkUnitPriority(JiraRestClient restClient) + throws InterruptedException, ExecutionException, TimeoutException { + log.info("Fetching work unit priorities for project {}", projectKey); + + try { + // Get all priorities through issues in the project + String jql = "project = " + projectKey; + SearchResult searchResult = restClient.getSearchClient() + .searchJql(jql, maxIssues, 0, null) + .get(timeoutSeconds, TimeUnit.SECONDS); + + // Track unique priorities + java.util.Map<String, BasicPriority> priorities = new java.util.HashMap<>(); + + // Extract priority from each issue + for (Issue issue : searchResult.getIssues()) { + if (issue.getPriority() != null) { + BasicPriority priority = issue.getPriority(); + priorities.putIfAbsent(priority.getId().toString(), priority); + } + } + + // Log priorities information + log.info("------------------------------------"); + log.info("Available Priorities:"); + + if (priorities.isEmpty()) { + log.info("No priorities found in project issues"); + } else { + for (BasicPriority priority : priorities.values()) { + log.info("------------------------------------"); + log.info("Priority ID: {}", priority.getId()); + log.info("Priority Name: {}", priority.getName()); + + // Try to get more information about the priority + try { + URI selfUri = priority.getSelf(); + if (selfUri != null) { + log.info("Priority URI: {}", selfUri); + } + } catch (Exception e) { + log.debug("Could not get priority URI: {}", e.getMessage()); + } + + // For BasicPriority, description might not be directly available + // We'll note this limitation + log.info("Priority Description: Not available in BasicPriority"); + + // Count issues with this priority + String priorityJql = "project = " + projectKey + " AND priority = \"" + priority.getName() + "\""; + SearchResult priorityResult = restClient.getSearchClient() + .searchJql(priorityJql, 1, 0, null) + .get(timeoutSeconds, TimeUnit.SECONDS); + log.info("Number of issues: {}", priorityResult.getTotal()); + } + + log.info("------------------------------------"); + log.info("Total priorities: {}", priorities.size()); + } + } catch (Exception e) { + log.error("Error fetching priority information: {}", e.getMessage(), e); + } } + + /// 6 returning types + /// 1) ChangItemBean + /// all_attributes -> WorkItemChange.description + /// 2) Worklog + /// timeSpent -> WorkItemChange.description + /// startDate -> WorkItemChange.description + /// 3) ChangeHistory + /// issue -> WorkItemChange.changedItem + /// 4) Comment + /// issue -> WorkItemChange.changedItem + /// 5) Resolution + /// name + description -> WorkItemChange.description + /// (if has Resolution) -> WorkItemChange.changedItem + /// 6) Issue + public void fetchChanges(JiraRestClient restClient) + throws InterruptedException, ExecutionException, TimeoutException { + log.info("Fetching issue changes, history, worklogs and comments for project {}", projectKey); + + try { + // Get a subset of issues to analyze changes + String jql = "project = " + projectKey + " ORDER BY updated DESC"; + SearchResult searchResult = restClient.getSearchClient() + .searchJql(jql, maxIssues, 0, null) + .get(timeoutSeconds, TimeUnit.SECONDS); + + log.info("Analyzing changes for {} issues (showing up to {})", searchResult.getTotal(), maxIssues); + + for (Issue issue : searchResult.getIssues()) { + log.info("------------------------------------"); + log.info("Issue: {} - {}", issue.getKey(), issue.getSummary()); + + // Fetch complete issue with all fields including changelog + Issue fullIssue = restClient.getIssueClient() + .getIssue(issue.getKey()) + .get(timeoutSeconds, TimeUnit.SECONDS); + + // Log resolution if available + if (fullIssue.getResolution() != null) { + Resolution resolution = fullIssue.getResolution(); + log.info("Resolution ID: {}", resolution.getId()); + log.info("Resolution Name: {}", resolution.getName()); + log.info("Resolution Description: {}", + resolution.getDescription() != null ? resolution.getDescription() : "None"); + } else { + log.info("Resolution: None (issue not resolved)"); + } + + // Extract Changelog/History using reflection (as it might vary between API versions) + try { + Object changelogObj = fullIssue.getClass().getMethod("getChangelog").invoke(fullIssue); + if (changelogObj != null) { + // Get entries/histories collection + Iterable<?> historiesObj = (Iterable<?>) changelogObj.getClass().getMethod("getHistories").invoke(changelogObj); + + for (Object historyObj : historiesObj) { + String authorName = "Unknown"; + String created = "Unknown"; + + try { + // Get author + Object authorObj = historyObj.getClass().getMethod("getAuthor").invoke(historyObj); + if (authorObj != null) { + authorName = (String) authorObj.getClass().getMethod("getDisplayName").invoke(authorObj); + } + + // Get creation date + Object createdObj = historyObj.getClass().getMethod("getCreated").invoke(historyObj); + if (createdObj != null) { + created = createdObj.toString(); + } + } catch (Exception e) { + // Skip if methods not available + } + + log.info("Change History - Author: {}, Created: {}", authorName, created); + log.info(" Issue: {}", issue.getKey()); + + // Get change items + try { + Iterable<?> itemsObj = (Iterable<?>) historyObj.getClass().getMethod("getItems").invoke(historyObj); + + for (Object itemObj : itemsObj) { + // Extract change item attributes + String field = getStringAttribute(itemObj, "getField"); + String fieldType = getStringAttribute(itemObj, "getFieldType"); + String from = getStringAttribute(itemObj, "getFrom"); + String fromString = getStringAttribute(itemObj, "getFromString"); + String to = getStringAttribute(itemObj, "getTo"); + String toString = getStringAttribute(itemObj, "getToString"); + + log.info(" ChangeItemBean:"); + log.info(" Field: {}", field); + log.info(" Field Type: {}", fieldType); + log.info(" From: {} ({})", from, fromString); + log.info(" To: {} ({})", to, toString); + } + } catch (Exception e) { + log.debug("Could not extract change items: {}", e.getMessage()); + } + } + } + } catch (Exception e) { + log.debug("Could not extract changelog: {}", e.getMessage()); + } + + // Process Worklogs + if (fullIssue.getWorklogs() != null && fullIssue.getWorklogs().iterator().hasNext()) { + log.info("Worklogs:"); + for (Worklog worklog : fullIssue.getWorklogs()) { + log.info(" Author: {}", worklog.getAuthor().getDisplayName()); + log.info(" Time Spent: {} ({}m)", worklog.getMinutesSpent() > 0 ? + formatWorkTime(worklog.getMinutesSpent()) : "Unknown", worklog.getMinutesSpent()); + log.info(" Start Date: {}", worklog.getStartDate() != null ? + worklog.getStartDate().toString("yyyy-MM-dd HH:mm:ss") : "Unknown"); + + if (worklog.getComment() != null && !worklog.getComment().isEmpty()) { + String comment = worklog.getComment(); + if (comment.length() > 100) { + comment = comment.substring(0, 97) + "..."; + } + log.info(" Comment: {}", comment); + } + } + } + + // Process Comments + if (fullIssue.getComments() != null && fullIssue.getComments().iterator().hasNext()) { + log.info("Comments:"); + for (Comment comment : fullIssue.getComments()) { + log.info(" Author: {}", comment.getAuthor().getDisplayName()); + log.info(" Created: {}", formatDateTime(comment.getCreationDate())); + log.info(" Updated: {}", formatDateTime(comment.getUpdateDate())); + log.info(" Issue: {}", issue.getKey()); + + String bodyText = comment.getBody(); + if (bodyText.length() > 100) { + bodyText = bodyText.substring(0, 97) + "..."; + } + log.info(" Body: {}", bodyText); + } + } + } + + log.info("------------------------------------"); + log.info("Change fetching completed"); + + } catch (Exception e) { + log.error("Error fetching changes: {}", e.getMessage(), e); + } + } + + /** + * Helper method to safely extract string attributes from objects using reflection + */ + private String getStringAttribute(Object obj, String methodName) { + try { + Object result = obj.getClass().getMethod(methodName).invoke(obj); + return result != null ? result.toString() : "null"; + } catch (Exception e) { + return "N/A"; + } + } + + /** + * Format minutes into human-readable work time (hours and minutes) + */ + private String formatWorkTime(int minutes) { + int hours = minutes / 60; + int mins = minutes % 60; + + if (hours > 0 && mins > 0) { + return hours + "h " + mins + "m"; + } else if (hours > 0) { + return hours + "h"; + } else { + return mins + "m"; + } + } + } \ No newline at end of file diff --git a/src/main/java/com/pump/jira/service/LabelService.java b/src/main/java/com/pump/jira/service/LabelService.java new file mode 100644 index 0000000000000000000000000000000000000000..5f9823764fb60cb695ab4c4518204ac2551cf5f8 --- /dev/null +++ b/src/main/java/com/pump/jira/service/LabelService.java @@ -0,0 +1,70 @@ +package com.pump.jira.service; + +import com.atlassian.jira.rest.client.api.JiraRestClient; +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.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +public class LabelService { + + @Value("${jira.project.key}") + private String projectKey; + + @Value("${jira.timeout.seconds}") + private int timeoutSeconds; + + /** + * Fetches all labels used in the project + * + * @param restClient The Jira REST client + */ + public void fetchLabels(JiraRestClient restClient) { + log.info("Fetching labels for project {}", projectKey); + + try { + // Search for issues with labels in the project + String jql = "project = " + projectKey + " AND labels IS NOT EMPTY ORDER BY updated DESC"; + SearchResult result = restClient.getSearchClient() + .searchJql(jql, 100, 0, null) + .get(timeoutSeconds, TimeUnit.SECONDS); + + Set<String> uniqueLabels = new HashSet<>(); + Map<String, Integer> labelCounts = new HashMap<>(); + + // Collect unique labels and count occurrences + for (Issue issue : result.getIssues()) { + if (issue.getLabels() != null && !issue.getLabels().isEmpty()) { + for (String label : issue.getLabels()) { + uniqueLabels.add(label); + labelCounts.put(label, labelCounts.getOrDefault(label, 0) + 1); + } + } + } + + // Display summary of all labels + log.info("Found {} unique labels in project {}:", uniqueLabels.size(), projectKey); + + // Sort labels by frequency + labelCounts.entrySet().stream() + .sorted(Map.Entry.<String, Integer>comparingByValue().reversed()) + .forEach(entry -> log.info(" Label: {} (used in {} issues)", entry.getKey(), entry.getValue())); + + // Note about custom field ID for labels + log.info("Labels in Jira are stored in system field 'labels'"); + log.info("Custom field ID: system (not a custom field)"); + + } catch (Exception e) { + log.error("Error fetching labels: {}", e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/pump/jira/service/ProjectService.java b/src/main/java/com/pump/jira/service/ProjectService.java index 331ec8fe6a79afd62006ec9c803f1872fa59acca..5cd98cb10acde02b2a684313138e575d4b20182a 100644 --- a/src/main/java/com/pump/jira/service/ProjectService.java +++ b/src/main/java/com/pump/jira/service/ProjectService.java @@ -2,16 +2,24 @@ package com.pump.jira.service; import com.atlassian.jira.rest.client.api.JiraRestClient; import com.atlassian.jira.rest.client.api.domain.BasicComponent; +import com.atlassian.jira.rest.client.api.domain.BasicProject; import com.atlassian.jira.rest.client.api.domain.IssueType; import com.atlassian.jira.rest.client.api.domain.Project; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; + @Slf4j @Service public class ProjectService { @@ -19,7 +27,7 @@ public class ProjectService { @Value("${jira.project.key}") private String projectKey; - @Value("${jira.timeout.seconds:30}") + @Value("${jira.timeout.seconds}") private int timeoutSeconds; /// id -> ToolProjectInstance.externalId @@ -127,4 +135,158 @@ public class ProjectService { log.error("Error fetching project information: {}", e.getMessage(), e); } } + + /// TODO - we have no ProjectCategory yet -> library does not support it + /// id -> DevelopmentProgram.externalId + /// name -> DevelopmentProgram.name + /// description -> DevelopmentProgram.description + /// min startDate -> DevelopmentProgram.startDate + /// where Project.projectCategory = this -> DevelopmentProgram.projects + public void fetchProjectCategory(JiraRestClient restClient) + throws InterruptedException, ExecutionException, TimeoutException { + log.info("Fetching project categories and their projects..."); + + try { + // Get all projects to analyze + Iterable<BasicProject> allProjects = restClient.getProjectClient() + .getAllProjects() + .get(timeoutSeconds, TimeUnit.SECONDS); + + // Maps to track categories and projects + Map<String, Map<String, Object>> categoriesMap = new HashMap<>(); + Map<String, List<Project>> projectsByCategory = new HashMap<>(); + + // Process each project to categorize it + for (BasicProject basicProject : allProjects) { + try { + Project project = restClient.getProjectClient() + .getProject(basicProject.getKey()) + .get(timeoutSeconds, TimeUnit.SECONDS); + + // Get category using reflection since direct API may not be available + Object categoryObj = null; + String categoryId = null; + String categoryName = null; + String categoryDescription = null; + + try { + categoryObj = project.getClass().getMethod("getProjectCategory").invoke(project); + if (categoryObj != null) { + categoryId = categoryObj.getClass().getMethod("getId").invoke(categoryObj).toString(); + categoryName = categoryObj.getClass().getMethod("getName").invoke(categoryObj).toString(); + try { + Object descObj = categoryObj.getClass().getMethod("getDescription").invoke(categoryObj); + categoryDescription = descObj != null ? descObj.toString() : null; + } catch (Exception e) { + // Description not available + } + } + } catch (Exception e) { + // ProjectCategory not available in this API version + } + + if (categoryId != null) { + // Create category info map if it doesn't exist + if (!categoriesMap.containsKey(categoryId)) { + Map<String, Object> categoryInfo = new HashMap<>(); + categoryInfo.put("id", categoryId); + categoryInfo.put("name", categoryName); + categoryInfo.put("description", categoryDescription); + categoriesMap.put(categoryId, categoryInfo); + } + + // Add project to category + projectsByCategory.computeIfAbsent(categoryId, k -> new ArrayList<>()).add(project); + } + } catch (Exception e) { + log.warn("Could not fetch details for project {}: {}", basicProject.getKey(), e.getMessage()); + } + } + + // Log information about each category and its projects + log.info("Found {} project categories", categoriesMap.size()); + + for (Map.Entry<String, Map<String, Object>> categoryEntry : categoriesMap.entrySet()) { + String categoryId = categoryEntry.getKey(); + Map<String, Object> category = categoryEntry.getValue(); + + log.info("------------------------------------"); + log.info("Category ID: {}", category.get("id")); + log.info("Category Name: {}", category.get("name")); + + if (category.get("description") != null) { + log.info("Category Description: {}", category.get("description")); + } else { + log.info("Category Description: None"); + } + + List<Project> projects = projectsByCategory.get(categoryId); + if (projects != null && !projects.isEmpty()) { + // Find earliest start date among projects + Date earliestStartDate = null; + + log.info("Projects in this category ({}):", projects.size()); + for (Project project : projects) { + log.info(" - {} ({})", project.getName(), project.getKey()); + + // Try to get start date using reflection + try { + Object startDateObj = project.getClass().getMethod("getStartDate").invoke(project); + if (startDateObj instanceof Date) { + Date startDate = (Date) startDateObj; + if (earliestStartDate == null || startDate.before(earliestStartDate)) { + earliestStartDate = startDate; + } + } + } catch (Exception e) { + // Start date not available + } + } + + if (earliestStartDate != null) { + log.info("Earliest Project Start Date: {}", new SimpleDateFormat("yyyy-MM-dd").format(earliestStartDate)); + } else { + log.info("No start dates available for projects in this category"); + } + } else { + log.info("No projects found in this category"); + } + } + + // Log projects without categories + log.info("------------------------------------"); + log.info("Projects without categories:"); + int uncategorizedCount = 0; + + for (BasicProject basicProject : allProjects) { + try { + Project project = restClient.getProjectClient() + .getProject(basicProject.getKey()) + .get(timeoutSeconds, TimeUnit.SECONDS); + + Object categoryObj = null; + try { + categoryObj = project.getClass().getMethod("getProjectCategory").invoke(project); + } catch (Exception e) { + // ProjectCategory not available + } + + if (categoryObj == null) { + uncategorizedCount++; + log.info(" - {} ({})", project.getName(), project.getKey()); + } + } catch (Exception e) { + log.warn("Could not fetch details for project {}: {}", basicProject.getKey(), e.getMessage()); + } + } + + if (uncategorizedCount == 0) { + log.info("All projects have been assigned to categories"); + } + + } catch (Exception e) { + log.error("Error fetching project categories: {}", e.getMessage(), e); + } + } + } \ No newline at end of file diff --git a/src/main/java/com/pump/jira/service/UserService.java b/src/main/java/com/pump/jira/service/UserService.java index 9e87adb34618d171f3d1b5339f944fc156d09049..3db7a7130103de58c430ebd15da6a9ab78b106a2 100644 --- a/src/main/java/com/pump/jira/service/UserService.java +++ b/src/main/java/com/pump/jira/service/UserService.java @@ -4,11 +4,15 @@ import com.atlassian.jira.rest.client.api.JiraRestClient; import com.atlassian.jira.rest.client.api.domain.BasicUser; import com.atlassian.jira.rest.client.api.domain.Issue; import com.atlassian.jira.rest.client.api.domain.Project; +import com.atlassian.jira.rest.client.api.domain.ProjectRole; +import com.atlassian.jira.rest.client.api.domain.RoleActor; 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 com.atlassian.jira.rest.client.api.domain.BasicProjectRole; +import java.net.URI; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -30,6 +34,9 @@ public class UserService { @Value("${jira.timeout.seconds}") private int timeoutSeconds; + @Value("${jira.url}") + private String jiraApiUrl; + /// TODO Most of the time return 401 Unauthorized - need to check permissions /// id -> Identity.externalId // TODO NEFUNGUJE PICO /// displayName -> Identity.name @@ -108,6 +115,145 @@ public class UserService { } } + /// id -> Role.externalId + /// name + Project.projectLead -> Role.name + /// descriptor -> Role.description + /// TODO WE HAVE NO PERMISSION TO FETCH LAST TWO PROPS + public void fetchProjectRoleActors(JiraRestClient restClient) + throws InterruptedException, ExecutionException, TimeoutException { + log.info("Fetching project role actors for project {}", projectKey); + + try { + // Get project for basic information and project lead + Project project = restClient.getProjectClient() + .getProject(projectKey) + .get(timeoutSeconds, TimeUnit.SECONDS); + // Log project lead information + BasicUser projectLead = project.getLead(); + if (projectLead != null) { + log.info("Project lead: {} ({})", projectLead.getDisplayName(), projectLead.getName()); + if (projectLead.getAccountId() != null) { + log.info("Project lead account ID: {}", projectLead.getAccountId()); + } + } else { + log.info("Project has no lead defined"); + } + // Get project roles directly from the project + log.info("Project roles for project {}:", projectKey); + Iterable<BasicProjectRole> projectRoles = project.getProjectRoles(); + + Map<String, Set<String>> userRoleMap = new HashMap<>(); + + // Process each role + // Process each role + for (BasicProjectRole role : projectRoles) { + log.info("------------------------------------"); + log.info("Role Name: {}, Role toString: {}", role.getName(), role.toString()); + } + + log.info("------------------------------------"); + log.info("Summary of users and their roles:"); + userRoleMap.forEach((userName, roles) -> { + log.info("User: {}, Roles: {}", userName, String.join(", ", roles)); + }); + + } catch (Exception e) { + log.error("Error fetching project role actors: {}", e.getMessage(), e); + e.printStackTrace(); + } + } + + /// TODO - requires LOG IN - 401 ERROR RN + /// self -> IdentityGroup.externalId + /// name -> IdentityGroup.name + /// GroupMembership.users -> IdentityGroup.members + public void fetchGroupBean(JiraRestClient restClient) + throws InterruptedException, ExecutionException, TimeoutException { + log.info("Fetching group information..."); + + try { + // Get all groups that have access to the project + Project project = restClient.getProjectClient() + .getProject(projectKey) + .get(timeoutSeconds, TimeUnit.SECONDS); + + // Get project roles to find group-based role actors + Iterable<BasicProjectRole> projectRoles = project.getProjectRoles(); + Set<String> groupNames = new HashSet<>(); + + // Process each role to identify groups + for (BasicProjectRole basicRole : projectRoles) { + try { + ProjectRole role = restClient.getProjectRolesRestClient() + .getRole(basicRole.getSelf()) + .get(timeoutSeconds, TimeUnit.SECONDS); + + Iterable<RoleActor> actors = role.getActors(); + if (actors != null) { + for (RoleActor actor : actors) { + if ("atlassian-group-role-actor".equals(actor.getType())) { + groupNames.add(actor.getName()); + log.info("Found group '{}' with role '{}'", actor.getName(), role.getName()); + } + } + } + } catch (Exception e) { + log.warn("Could not fetch role details for {}: {}", basicRole.getName(), e.getMessage()); + } + } + + // Log the groups we found + if (!groupNames.isEmpty()) { + log.info("Found {} groups with access to this project", groupNames.size()); + + // For each group, try to get membership details + for (String groupName : groupNames) { + try { + log.info("------------------------------------"); + log.info("Group Name: {}", groupName); + log.info("Group Self: {}/rest/api/2/group?groupname={}", jiraApiUrl, groupName); + + // Note: Direct group membership API might require admin permissions + // and might not be available through the standard REST client + log.info("Group Members: To see complete membership, admin privileges may be required"); + + // Try to get users with this group through search + String jql = String.format("project = %s AND memberOf(\"%s\")", projectKey, groupName); + SearchResult searchResult = restClient.getSearchClient() + .searchJql(jql) + .get(timeoutSeconds, TimeUnit.SECONDS); + + Set<BasicUser> groupUsers = new HashSet<>(); + for (Issue issue : searchResult.getIssues()) { + if (issue.getReporter() != null) { + groupUsers.add(issue.getReporter()); + } + if (issue.getAssignee() != null) { + groupUsers.add(issue.getAssignee()); + } + } + + if (!groupUsers.isEmpty()) { + log.info("Users found in group (may be incomplete):"); + for (BasicUser user : groupUsers) { + log.info(" - {} ({})", user.getDisplayName(), user.getName()); + } + } else { + log.info("No users found in group through JQL search"); + } + + } catch (Exception e) { + log.warn("Error fetching details for group {}: {}", groupName, e.getMessage()); + } + } + } else { + log.info("No groups found with access to this project"); + } + + } catch (Exception e) { + log.error("Error fetching group information: {}", e.getMessage(), e); + } + } } diff --git a/src/main/java/com/pump/jira/service/VersionService.java b/src/main/java/com/pump/jira/service/VersionService.java index 2a12230ca74fc3180d9204e825361f3e8f01761d..d7bcb472f2534ea6519150bf44446067e192dedc 100644 --- a/src/main/java/com/pump/jira/service/VersionService.java +++ b/src/main/java/com/pump/jira/service/VersionService.java @@ -71,4 +71,111 @@ public class VersionService { } } + + /// id -> Phase.externalId + /// name -> Phase.name + /// description + archived + released+ sequence -> Phase.description + /// startDate -> Phase.startDate + /// releaseDate -> Phase.endDate + /// project -> Phase.project + /// min Issue.created -> Phase.created + public void fetchVersion(JiraRestClient restClient) + throws InterruptedException, ExecutionException, TimeoutException { + log.info("Fetching detailed version information for project {}", projectKey); + + try { + Project project = restClient.getProjectClient() + .getProject(projectKey) + .get(timeoutSeconds, TimeUnit.SECONDS); + + // Convert Iterable to List for easier handling + List<Version> versions = new ArrayList<>(); + project.getVersions().forEach(versions::add); + + if (versions.isEmpty()) { + log.info("No versions found for project {}", projectKey); + return; + } + + log.info("Found {} versions for project {}", versions.size(), projectKey); + + for (Version version : versions) { + log.info("------------------------------------"); + log.info("Version ID: {}", version.getId()); + log.info("Version Name: {}", version.getName()); + + // Description and status flags + StringBuilder descriptionWithMeta = new StringBuilder(); + if (version.getDescription() != null) { + descriptionWithMeta.append(version.getDescription()); + } + + // Add metadata to description + descriptionWithMeta.append(" [Archived: ") + .append(version.isArchived()) + .append(", Released: ") + .append(version.isReleased()); + + // Try to get sequence number using reflection if available + try { + Object sequenceObj = version.getClass().getMethod("getSequence").invoke(version); + if (sequenceObj != null) { + descriptionWithMeta.append(", Sequence: ").append(sequenceObj); + } + } catch (Exception e) { + // Sequence not available + } + + descriptionWithMeta.append("]"); + log.info("Full Description: {}", descriptionWithMeta); + + // TODO Start date - some versions may not have a start date +// if (version.getStartDate() != null) { +// log.info("Start Date: {}", version.getStartDate()); +// } else { +// log.info("Start Date: Not set"); +// } + + // Release date + if (version.getReleaseDate() != null) { + log.info("Release Date: {}", version.getReleaseDate()); + } else { + log.info("Release Date: Not set"); + } + + // Project information + log.info("Project Key: {}", project.getKey()); + log.info("Project Name: {}", project.getName()); + log.info("Project ID: {}", project.getId()); + + // Find earliest issue creation date for this version + String jql = "project = " + projectKey + " AND fixVersion = '" + version.getName() + "' ORDER BY created ASC"; + + try { + // Search for issues with this version as a fix version, sorted by creation date + com.atlassian.jira.rest.client.api.domain.SearchResult searchResult = + restClient.getSearchClient() + .searchJql(jql, 1, 0, null) + .get(timeoutSeconds, TimeUnit.SECONDS); + + // If we have any issues + if (searchResult.getTotal() > 0 && searchResult.getIssues().iterator().hasNext()) { + com.atlassian.jira.rest.client.api.domain.Issue earliestIssue = + searchResult.getIssues().iterator().next(); + + log.info("Earliest Issue Creation Date: {}", earliestIssue.getCreationDate()); + log.info("Earliest Issue Key: {}", earliestIssue.getKey()); + log.info("Total Issues: {}", searchResult.getTotal()); + } else { + log.info("No issues found for this version"); + } + } catch (Exception e) { + log.warn("Error finding issues for version {}: {}", version.getName(), e.getMessage()); + } + } + } catch (Exception e) { + log.error("Error fetching version information: {}", e.getMessage(), e); + } + } + } \ No newline at end of file