From 990aa99dfe0b697ab4395cfb7f5e5a4738b49390 Mon Sep 17 00:00:00 2001 From: marko-bekhta Date: Mon, 9 Dec 2024 17:19:34 +0100 Subject: [PATCH] Use project group context for handling events instead of a project context. and remove dependency on project key in the requests --- .../infra/replicate/jira/JiraConfig.java | 10 +- .../resource/JiraWebHookListenerResource.java | 17 +- .../service/jira/HandlerProjectContext.java | 117 ++++------- .../jira/HandlerProjectGroupContext.java | 106 +++++++++- .../jira/service/jira/JiraService.java | 185 ++++++------------ .../service/jira/client/JiraRestClient.java | 5 + .../jira/client/JiraRestClientBuilder.java | 6 + .../JiraCommentDeleteEventHandler.java | 13 +- .../jira/handler/JiraCommentEventHandler.java | 4 +- .../JiraCommentUpsertEventHandler.java | 13 +- .../jira/handler/JiraEventHandler.java | 16 +- .../JiraIssueAbstractEventHandler.java | 34 ++-- .../handler/JiraIssueDeleteEventHandler.java | 10 +- ...JiraIssueInternalAbstractEventHandler.java | 13 +- .../JiraIssueLinkDeleteEventHandler.java | 15 +- .../JiraIssueLinkUpsertEventHandler.java | 41 ++-- .../JiraIssueSimpleUpsertEventHandler.java | 13 +- .../JiraIssueTransitionOnlyEventHandler.java | 9 +- .../handler/JiraIssueUpsertEventHandler.java | 17 +- .../JiraVersionUpsertEventHandler.java | 8 +- ...JiraAbstractVersionActionEventHandler.java | 9 +- .../action/JiraActionEventHandler.java | 11 +- .../JiraAffectsVersionActionEventHandler.java | 4 +- .../JiraAssigneeActionEventHandler.java | 9 +- .../JiraFixVersionActionEventHandler.java | 4 +- .../JiraTransitionActionEventHandler.java | 9 +- .../jira/model/action/JiraActionEvent.java | 5 +- .../jira/model/hook/JiraActionEventType.java | 12 +- .../jira/model/hook/JiraWebhookEventType.java | 24 +-- .../service/jira/model/rest/JiraFields.java | 2 +- .../service/jira/model/rest/JiraProject.java | 24 +++ .../validation/ConfiguredProjectGroup.java | 28 +++ .../ConfiguredProjectGroupValidator.java | 32 +++ .../validation/ConfiguredProjectsService.java | 12 +- .../validation/RequestSignatureFilter.java | 16 +- .../replicate/jira/SimpleProjectHookTest.java | 11 +- .../jira/export/ExportProjectTest.java | 2 +- .../replicate/jira/handler/IssueTest.java | 16 +- .../jira/mock/SampleJiraRestClient.java | 9 + .../RequestSignatureFilterTest.java | 16 +- src/test/resources/application.properties | 6 +- 41 files changed, 516 insertions(+), 397 deletions(-) create mode 100644 src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraProject.java create mode 100644 src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProjectGroup.java create mode 100644 src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProjectGroupValidator.java diff --git a/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java b/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java index 463b036..4aa9cfa 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java @@ -98,6 +98,11 @@ interface JiraProjectGroup { * Allows customizing formatting options. */ Formatting formatting(); + + /** + * Allows enabling signature verification. + */ + WebHookSecurity security(); } interface Formatting { @@ -188,11 +193,6 @@ interface JiraProject { */ String originalProjectKey(); - /** - * Allows enabling signature verification. - */ - WebHookSecurity security(); - /** * Allows enabling signature verification of downstream events. */ diff --git a/src/main/java/org/hibernate/infra/replicate/jira/resource/JiraWebHookListenerResource.java b/src/main/java/org/hibernate/infra/replicate/jira/resource/JiraWebHookListenerResource.java index 6f4ddeb..ba7b0c5 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/resource/JiraWebHookListenerResource.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/resource/JiraWebHookListenerResource.java @@ -4,6 +4,7 @@ import org.hibernate.infra.replicate.jira.service.jira.model.action.JiraActionEvent; import org.hibernate.infra.replicate.jira.service.jira.model.hook.JiraWebHookEvent; import org.hibernate.infra.replicate.jira.service.validation.ConfiguredProject; +import org.hibernate.infra.replicate.jira.service.validation.ConfiguredProjectGroup; import org.jboss.resteasy.reactive.RestPath; @@ -23,22 +24,22 @@ public class JiraWebHookListenerResource { JiraService jiraService; @POST - @Path("/{project}") + @Path("/source/{projectGroup}") @Consumes(MediaType.APPLICATION_JSON) - public String somethingHappenedUpstream(@RestPath @NotNull @ConfiguredProject String project, + public String somethingHappenedUpstream(@RestPath @NotNull @ConfiguredProjectGroup String projectGroup, @QueryParam("triggeredByUser") String triggeredByUser, JiraWebHookEvent event) { - Log.tracef("Received a notification about %s project: %.200s...", project, event); - jiraService.acknowledge(project, event, triggeredByUser); + Log.tracef("Received a notification about %s project group: %.200s...", projectGroup, event); + jiraService.acknowledge(projectGroup, event, triggeredByUser); return "ack"; } @POST - @Path("/mirror/{project}") + @Path("/mirror/{projectGroup}/{project}") @Consumes(MediaType.APPLICATION_JSON) - public String somethingHappenedDownstream(@RestPath @NotNull @ConfiguredProject String project, - JiraActionEvent data) { + public String somethingHappenedDownstream(@RestPath @NotNull @ConfiguredProjectGroup String projectGroup, + @RestPath @NotNull @ConfiguredProject String project, JiraActionEvent data) { Log.tracef("Received a downstream notification about %s project: %s...", project, data); - jiraService.downstreamAcknowledge(project, data); + jiraService.downstreamAcknowledge(projectGroup, data); return "ack"; } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java index b3001ba..4145309 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java @@ -1,7 +1,5 @@ package org.hibernate.infra.replicate.jira.service.jira; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -19,7 +17,6 @@ import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssueBulk; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssueBulkResponse; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssues; -import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; import io.quarkus.logging.Log; @@ -34,46 +31,26 @@ public final class HandlerProjectContext implements AutoCloseable { private final ReentrantLock versionLock = new ReentrantLock(); private final String projectName; - private final String projectGroupName; - private final JiraRestClient sourceJiraClient; - private final JiraRestClient destinationJiraClient; private final HandlerProjectGroupContext projectGroupContext; private final JiraConfig.JiraProject project; private final AtomicLong currentIssueKeyNumber; private final JiraIssueBulk bulk; private final String projectKeyWithDash; - private final JiraUser notMappedAssignee; - - private final Map allProjectsContextMap; - private final Pattern sourceLabelPattern; - private final DateTimeFormatter formatter; - private final Map destFixVersions; + private final Pattern keyToUpdatePattern; - public HandlerProjectContext(String projectName, String projectGroupName, JiraRestClient sourceJiraClient, - JiraRestClient destinationJiraClient, HandlerProjectGroupContext projectGroupContext, - Map allProjectsContextMap) { + public HandlerProjectContext(String projectName, HandlerProjectGroupContext projectGroupContext) { this.projectName = projectName; - this.projectGroupName = projectGroupName; - this.sourceJiraClient = sourceJiraClient; - this.destinationJiraClient = destinationJiraClient; this.projectGroupContext = projectGroupContext; this.project = projectGroup().projects().get(projectName()); this.currentIssueKeyNumber = new AtomicLong(getCurrentLatestJiraIssueKeyNumber()); this.bulk = new JiraIssueBulk(createIssuePlaceholder(), ISSUES_PER_REQUEST); this.projectKeyWithDash = "%s-".formatted(project.projectKey()); - this.notMappedAssignee = projectGroup().users().notMappedAssignee() - .map(v -> new JiraUser(projectGroup().users().mappedPropertyName(), v)).orElse(null); - - this.allProjectsContextMap = allProjectsContextMap; - this.sourceLabelPattern = Pattern - .compile(projectGroupContext.projectGroup().formatting().labelTemplate().formatted(".+")); - this.formatter = DateTimeFormatter.ofPattern(projectGroupContext.projectGroup().formatting().timestampFormat()); - - this.destFixVersions = getAndCreateMissingCurrentFixVersions(project, projectGroupContext, sourceJiraClient, - destinationJiraClient); + this.destFixVersions = getAndCreateMissingCurrentFixVersions(project, projectGroupContext, + projectGroupContext.sourceJiraClient(), projectGroupContext.destinationJiraClient()); + this.keyToUpdatePattern = Pattern.compile("^%s-\\d+".formatted(project.originalProjectKey())); } public JiraConfig.JiraProject project() { @@ -84,18 +61,6 @@ public String projectName() { return projectName; } - public String projectGroupName() { - return projectGroupName; - } - - public JiraRestClient sourceJiraClient() { - return sourceJiraClient; - } - - public JiraRestClient destinationJiraClient() { - return destinationJiraClient; - } - public JiraConfig.JiraProjectGroup projectGroup() { return projectGroupContext.projectGroup(); } @@ -105,8 +70,9 @@ public AtomicLong currentIssueKeyNumber() { } public Long getLargestSyncedJiraIssueKeyNumber() { - JiraIssues issues = destinationJiraClient.find("project = %s and summary !~\"%s\" ORDER BY key DESC" - .formatted(project.projectId(), SYNC_ISSUE_PLACEHOLDER_SUMMARY), 0, 1); + JiraIssues issues = projectGroupContext.destinationJiraClient() + .find("project = %s and summary !~\"%s\" ORDER BY key DESC".formatted(project.projectId(), + SYNC_ISSUE_PLACEHOLDER_SUMMARY), 0, 1); if (issues.issues.isEmpty()) { return 0L; } else { @@ -123,7 +89,7 @@ public Optional getNextIssueToSync(Long latestSyncedJiraIssueKeyNumbe query = "project = %s ORDER BY key ASC".formatted(project.originalProjectKey(), project.originalProjectKey()); } - JiraIssues issues = sourceJiraClient.find(query, 0, 1); + JiraIssues issues = projectGroupContext.sourceJiraClient().find(query, 0, 1); if (issues.issues.isEmpty()) { return Optional.empty(); } else { @@ -133,7 +99,7 @@ public Optional getNextIssueToSync(Long latestSyncedJiraIssueKeyNumbe private Long getCurrentLatestJiraIssueKeyNumber() { try { - JiraIssues issues = destinationJiraClient + JiraIssues issues = projectGroupContext.destinationJiraClient() .find("project = %s ORDER BY created DESC".formatted(project.projectId()), 0, 1); if (issues.issues.isEmpty()) { return 0L; @@ -165,7 +131,7 @@ public void createNextPlaceholderBatch(Long upToKeyNumber) { } try { do { - JiraIssueBulkResponse response = destinationJiraClient.create(bulk); + JiraIssueBulkResponse response = projectGroupContext.destinationJiraClient().create(bulk); response.issues.stream().mapToLong(i -> JiraIssue.keyToLong(i.key)).max() .ifPresent(currentIssueKeyNumber::set); Log.infof( @@ -185,10 +151,6 @@ public void startProcessingDownstreamEvent() throws InterruptedException { projectGroupContext.startProcessingDownstreamEvent(); } - public JiraUser notMappedAssignee() { - return notMappedAssignee; - } - private boolean requiredIssueKeyNumberShouldBeAvailable(Long key) { return currentIssueKeyNumber.get() >= key; } @@ -214,15 +176,13 @@ private JiraIssue createIssuePlaceholder() { @Override public String toString() { - return "HandlerProjectContext[" + "projectName=" + projectName + ", " + "projectGroupName=" + projectGroupName - + ", " + "sourceJiraClient=" + sourceJiraClient + ", " + "destinationJiraClient=" - + destinationJiraClient + ", " + "projectGroup=" + projectGroupContext.projectGroup() + ", " - + "currentIssueKeyNumber=" + currentIssueKeyNumber + ']'; + return "HandlerProjectContext[" + "projectName=" + projectName + ", " + "projectGroup=" + + projectGroupContext.projectGroup() + ", " + "currentIssueKeyNumber=" + currentIssueKeyNumber + ']'; } @Override public void close() { - projectGroupContext.close(); + // do nothing } public int pendingEventsInCurrentContext() { @@ -241,22 +201,6 @@ public void submitDownstreamTask(Runnable runnable) { projectGroupContext.submitDownstreamTask(runnable); } - public Optional contextForProjectInSameGroup(String project) { - if (!projectGroup().projects().containsKey(project)) { - // different project group, don't bother - return Optional.empty(); - } - return Optional.ofNullable(allProjectsContextMap.get(project)); - } - - public boolean isSourceLabel(String label) { - return sourceLabelPattern.matcher(label).matches(); - } - - public String formatTimestamp(ZonedDateTime time) { - return time != null ? time.format(formatter) : ""; - } - public JiraVersion fixVersion(JiraVersion version) { return fixVersion(version, false); } @@ -271,11 +215,13 @@ public JiraVersion fixVersion(JiraVersion version, boolean force) { versionLock.lock(); try { if (force) { - return destFixVersions.compute(version.name, (name, current) -> upsert(project, projectGroupContext, - destinationJiraClient, version, destinationJiraClient().versions(project.projectKey()))); + return destFixVersions.compute(version.name, + (name, current) -> upsert(project, projectGroupContext, + projectGroupContext.destinationJiraClient(), version, + projectGroupContext.destinationJiraClient().versions(project.projectKey()))); } else { - return destFixVersions.computeIfAbsent(version.name, - name -> upsert(project, projectGroupContext, destinationJiraClient, version, List.of())); + return destFixVersions.computeIfAbsent(version.name, name -> upsert(project, projectGroupContext, + projectGroupContext.destinationJiraClient(), version, List.of())); } } catch (Exception e) { Log.errorf(e, @@ -291,8 +237,8 @@ public void refreshFixVersions() { versionLock.lock(); try { destFixVersions.clear(); - destFixVersions.putAll(getAndCreateMissingCurrentFixVersions(project, projectGroupContext, sourceJiraClient, - destinationJiraClient)); + destFixVersions.putAll(getAndCreateMissingCurrentFixVersions(project, projectGroupContext, + projectGroupContext.sourceJiraClient(), projectGroupContext.destinationJiraClient())); } finally { versionLock.unlock(); } @@ -360,14 +306,6 @@ private static boolean versionNeedsUpdate(JiraVersion upstreamVersion, JiraVersi || !Objects.equals(upstreamVersion.releaseDate, downstreamVersion.releaseDate); } - public boolean isUserIgnored(String triggeredByUser) { - return projectGroupContext.projectGroup().users().ignoredUpstreamUsers().contains(triggeredByUser); - } - - public boolean isDownstreamUserIgnored(String triggeredByUser) { - return projectGroupContext.projectGroup().users().ignoredDownstreamUsers().contains(triggeredByUser); - } - public String upstreamUser(String mappedValue) { return projectGroupContext.upstreamUser(mappedValue); } @@ -375,4 +313,15 @@ public String upstreamUser(String mappedValue) { public String upstreamStatus(String mappedValue) { return projectGroupContext.upstreamStatus(mappedValue); } + + public String toDestinationKey(String key) { + if (keyToUpdatePattern.matcher(key).matches()) { + return "%s-%d".formatted(project().projectKey(), JiraIssue.keyToLong(key)); + } + return key; + } + + public String toSourceKey(String key) { + return "%s-%d".formatted(project().originalProjectKey(), JiraIssue.keyToLong(key)); + } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectGroupContext.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectGroupContext.java index bb8cfee..0024421 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectGroupContext.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectGroupContext.java @@ -1,8 +1,11 @@ package org.hibernate.infra.replicate.jira.service.jira; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingDeque; @@ -11,8 +14,11 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; +import java.util.regex.Pattern; import org.hibernate.infra.replicate.jira.JiraConfig; +import org.hibernate.infra.replicate.jira.service.jira.client.JiraRestClient; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser; import io.quarkus.logging.Log; @@ -21,6 +27,10 @@ public final class HandlerProjectGroupContext implements AutoCloseable { private final ExecutorService eventHandlingExecutor; private final Supplier workQueueSize; + private final String projectGroupName; + private final JiraRestClient sourceJiraClient; + private final JiraRestClient destinationJiraClient; + private final ExecutorService downstreamEventHandlingExecutor; private final Supplier downstreamWorkQueueSize; private final ScheduledExecutorService rateLimiterExecutor = Executors.newScheduledThreadPool(1); @@ -29,8 +39,14 @@ public final class HandlerProjectGroupContext implements AutoCloseable { private final JiraConfig.JiraProjectGroup projectGroup; private final Map invertedUsers; private final Map invertedStatuses; - - public HandlerProjectGroupContext(JiraConfig.JiraProjectGroup projectGroup) { + private final Map projectContexts; + private final Pattern sourceLabelPattern; + private final JiraUser notMappedAssignee; + private final DateTimeFormatter formatter; + + public HandlerProjectGroupContext(String projectGroupName, JiraConfig.JiraProjectGroup projectGroup, + JiraRestClient source, JiraRestClient destination) { + this.projectGroupName = projectGroupName; this.projectGroup = projectGroup; JiraConfig.EventProcessing processing = projectGroup.processing(); @@ -57,6 +73,21 @@ public HandlerProjectGroupContext(JiraConfig.JiraProjectGroup projectGroup) { this.invertedUsers = invert(projectGroup.users().mapping()); this.invertedStatuses = invert(projectGroup.statuses().mapping()); + this.sourceJiraClient = source; + this.destinationJiraClient = destination; + + Map projectContexts = new HashMap<>(); + + for (var project : projectGroup.projects().entrySet()) { + projectContexts.put(project.getKey(), new HandlerProjectContext(project.getKey(), this)); + } + this.projectContexts = Collections.unmodifiableMap(projectContexts); + + this.notMappedAssignee = projectGroup().users().notMappedAssignee() + .map(v -> new JiraUser(projectGroup().users().mappedPropertyName(), v)).orElse(null); + + this.sourceLabelPattern = Pattern.compile(projectGroup().formatting().labelTemplate().formatted(".+")); + this.formatter = DateTimeFormatter.ofPattern(projectGroup().formatting().timestampFormat()); } private static Map invert(Map map) { @@ -75,6 +106,10 @@ public void startProcessingDownstreamEvent() throws InterruptedException { downstreamRateLimiter.acquire(1); } + public String projectGroupName() { + return projectGroupName; + } + public JiraConfig.JiraProjectGroup projectGroup() { return projectGroup; } @@ -95,8 +130,55 @@ public void submitDownstreamTask(Runnable task) { downstreamEventHandlingExecutor.submit(task); } + public JiraRestClient sourceJiraClient() { + return sourceJiraClient; + } + + public JiraRestClient destinationJiraClient() { + return destinationJiraClient; + } + + public HandlerProjectContext contextForProject(String project) { + HandlerProjectContext context = projectContexts.get(project); + if (context == null) { + throw new IllegalStateException("No handler context for project %s".formatted(project)); + } + return context; + } + + public Optional findContextForProject(String project) { + if (!projectGroup().projects().containsKey(project)) { + // different project group, don't bother + return Optional.empty(); + } + return Optional.ofNullable(projectContexts.get(project)); + } + + public Optional findContextForOriginalProjectKey(String project) { + + for (HandlerProjectContext context : projectContexts.values()) { + if (context.project().originalProjectKey().equals(project)) { + return Optional.of(context); + } + } + + return Optional.empty(); + } + + public HandlerProjectContext contextForOriginalProjectKey(String project) { + + for (HandlerProjectContext context : projectContexts.values()) { + if (context.project().originalProjectKey().equals(project)) { + return context; + } + } + + throw new IllegalStateException("No handler context for project %s".formatted(project)); + } + @Override public void close() { + projectContexts.values().forEach(HandlerProjectContext::close); // when requesting to close the context we aren't expecting to process any other // events hence there's no point in continuing "releasing" more "permits": if (!rateLimiterExecutor.isShutdown()) { @@ -127,4 +209,24 @@ public String upstreamStatus(String mappedValue) { return invertedStatuses.get(mappedValue); } + public boolean isUserIgnored(String triggeredByUser) { + return projectGroup().users().ignoredUpstreamUsers().contains(triggeredByUser); + } + + public boolean isDownstreamUserIgnored(String triggeredByUser) { + return projectGroup().users().ignoredDownstreamUsers().contains(triggeredByUser); + } + + public JiraUser notMappedAssignee() { + return notMappedAssignee; + } + + public boolean isSourceLabel(String label) { + return sourceLabelPattern.matcher(label).matches(); + } + + public String formatTimestamp(ZonedDateTime time) { + return time != null ? time.format(formatter) : ""; + } + } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java index 604bc10..897c964 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java @@ -36,13 +36,11 @@ import io.quarkus.scheduler.Scheduled; import io.quarkus.scheduler.Scheduler; import io.quarkus.vertx.http.ManagementInterface; -import io.vertx.core.json.JsonObject; import jakarta.annotation.PreDestroy; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; import jakarta.validation.ConstraintViolationException; -import jakarta.ws.rs.core.MediaType; @ApplicationScoped public class JiraService { @@ -50,24 +48,22 @@ public class JiraService { private static final String SYSTEM_USER = "94KJcxFzgxZlXyTss4oR0rDNqtjwjhIiZLzYNx0Mwuc="; private final ReportingConfig reportingConfig; - private final Map contextPerProject; + private final Map contextPerProjectGroup; private final JiraConfig jiraConfig; private final Scheduler scheduler; @Inject public JiraService(JiraConfig jiraConfig, ReportingConfig reportingConfig, Scheduler scheduler) { - Map contextMap = new HashMap<>(); + Map contextMap = new HashMap<>(); for (var entry : jiraConfig.projectGroup().entrySet()) { JiraRestClient source = JiraRestClientBuilder.of(entry.getValue().source()); JiraRestClient destination = JiraRestClientBuilder.of(entry.getValue().destination()); - HandlerProjectGroupContext groupContext = new HandlerProjectGroupContext(entry.getValue()); - for (var project : entry.getValue().projects().entrySet()) { - contextMap.put(project.getKey(), new HandlerProjectContext(project.getKey(), entry.getKey(), source, - destination, groupContext, contextMap)); - } + HandlerProjectGroupContext groupContext = new HandlerProjectGroupContext(entry.getKey(), entry.getValue(), + source, destination); + contextMap.put(entry.getKey(), groupContext); } - this.contextPerProject = Collections.unmodifiableMap(contextMap); + this.contextPerProjectGroup = Collections.unmodifiableMap(contextMap); this.reportingConfig = reportingConfig; configureScheduledTasks(scheduler, jiraConfig); @@ -86,55 +82,32 @@ private void configureScheduledTasks(Scheduler scheduler, JiraConfig jiraConfig) } public void registerManagementRoutes(@Observes ManagementInterface mi) { - mi.router().get("/sync/issues/init").consumes(MediaType.APPLICATION_JSON).blockingHandler(rc -> { - JsonObject request = rc.body().asJsonObject(); - String project = request.getString("project"); - - HandlerProjectContext context = contextPerProject.get(project); - - if (context == null) { - throw new IllegalArgumentException("Unknown project '%s'".formatted(project)); - } - - AtomicLong largestSyncedJiraIssueKeyNumber = new AtomicLong(context.getLargestSyncedJiraIssueKeyNumber()); - - String identity = "Init Sync for project %s".formatted(project); - scheduler.newJob(identity).setConcurrentExecution(Scheduled.ConcurrentExecution.SKIP) - // every 10 seconds: - .setCron("0/10 * * * * ?").setTask(executionContext -> { - Optional issueToSync = context - .getNextIssueToSync(largestSyncedJiraIssueKeyNumber.get()); - if (issueToSync.isEmpty()) { - scheduler.unscheduleJob(identity); - } else { - triggerSyncEvent(issueToSync.get(), context); - largestSyncedJiraIssueKeyNumber.set(JiraIssue.keyToLong(issueToSync.get().key)); - } - }).schedule(); - rc.end(); - }); - mi.router().get("/sync/issues/init/:project").blockingHandler(rc -> { + mi.router().get("/sync/issues/init/:projectGroup/:project").blockingHandler(rc -> { // TODO: we can remove this one once we figure out why POST management does not // work correctly... + String projectGroup = rc.pathParam("projectGroup"); String project = rc.pathParam("project"); List maxToSyncList = rc.queryParam("maxToSync"); AtomicInteger maxToSync = maxToSyncList.isEmpty() ? null : new AtomicInteger(Integer.parseInt(maxToSyncList.get(0)) + 1); - HandlerProjectContext context = contextPerProject.get(project); + HandlerProjectGroupContext context = contextPerProjectGroup.get(projectGroup); if (context == null) { - throw new IllegalArgumentException("Unknown project '%s'".formatted(project)); + throw new IllegalArgumentException("Unknown project group '%s'".formatted(projectGroup)); } - AtomicLong largestSyncedJiraIssueKeyNumber = new AtomicLong(context.getLargestSyncedJiraIssueKeyNumber()); + HandlerProjectContext projectContext = context.contextForOriginalProjectKey(project); + + AtomicLong largestSyncedJiraIssueKeyNumber = new AtomicLong( + projectContext.getLargestSyncedJiraIssueKeyNumber()); BooleanSupplier continueSyncing = maxToSync == null ? () -> true : () -> maxToSync.decrementAndGet() > 0; String identity = "Init Sync for project %s".formatted(project); scheduler.newJob(identity).setConcurrentExecution(Scheduled.ConcurrentExecution.SKIP) // every 10 seconds: .setCron("0/10 * * * * ?").setTask(executionContext -> { - Optional issueToSync = context + Optional issueToSync = projectContext .getNextIssueToSync(largestSyncedJiraIssueKeyNumber.get()); if (issueToSync.isEmpty() || !continueSyncing.getAsBoolean()) { scheduler.unscheduleJob(identity); @@ -145,29 +118,29 @@ public void registerManagementRoutes(@Observes ManagementInterface mi) { }).schedule(); rc.end(); }); - mi.router().get("/sync/issues/re-sync/:project/:issue").blockingHandler(rc -> { + mi.router().get("/sync/issues/re-sync/:projectGroup/:issue").blockingHandler(rc -> { // TODO: we can remove this one once we figure out why POST management does not // work correctly... - String project = rc.pathParam("project"); + String projectGroup = rc.pathParam("projectGroup"); String issue = rc.pathParam("issue"); - HandlerProjectContext context = contextPerProject.get(project); + HandlerProjectGroupContext context = contextPerProjectGroup.get(projectGroup); if (context == null) { - throw new IllegalArgumentException("Unknown project '%s'".formatted(project)); + throw new IllegalArgumentException("Unknown project '%s'".formatted(projectGroup)); } triggerSyncEvent(context.sourceJiraClient().getIssue(issue), context); rc.end(); }); - mi.router().get("/sync/issues/deleted/:project").blockingHandler(rc -> { - String project = rc.pathParam("project"); + mi.router().get("/sync/issues/deleted/:projectGroup").blockingHandler(rc -> { + String projectGroup = rc.pathParam("projectGroup"); String issues = rc.queryParam("issues").getFirst(); - HandlerProjectContext context = contextPerProject.get(project); + HandlerProjectGroupContext context = contextPerProjectGroup.get(projectGroup); if (context == null) { - throw new IllegalArgumentException("Unknown project '%s'".formatted(project)); + throw new IllegalArgumentException("Unknown project '%s'".formatted(projectGroup)); } String[] split = issues.split(","); @@ -176,16 +149,16 @@ public void registerManagementRoutes(@Observes ManagementInterface mi) { } rc.end(); }); - mi.router().get("/sync/issues/transition/re-sync/:project").blockingHandler(rc -> { + mi.router().get("/sync/issues/transition/re-sync/:projectGroup").blockingHandler(rc -> { // TODO: we can remove this one once we figure out why POST management does not // work correctly... - String project = rc.pathParam("project"); + String projectGroup = rc.pathParam("projectGroup"); String query = rc.queryParam("query").getFirst(); - HandlerProjectContext context = contextPerProject.get(project); + HandlerProjectGroupContext context = contextPerProjectGroup.get(projectGroup); if (context == null) { - throw new IllegalArgumentException("Unknown project '%s'".formatted(project)); + throw new IllegalArgumentException("Unknown project '%s'".formatted(projectGroup)); } context.submitTask(() -> { @@ -194,49 +167,30 @@ public void registerManagementRoutes(@Observes ManagementInterface mi) { }); rc.end(); }); - mi.router().post("/sync/issues/list").consumes(MediaType.APPLICATION_JSON).blockingHandler(rc -> { - // sync issues based on a list of issue-keys supplied in the JSON body: - JsonObject request = rc.body().asJsonObject(); - String project = request.getString("project"); - List issueKeys = request.getJsonArray("issues").stream().map(Objects::toString).toList(); - - HandlerProjectContext context = contextPerProject.get(project); - - if (context == null) { - throw new IllegalArgumentException("Unknown project '%s'".formatted(project)); - } - - context.submitTask(() -> { - for (String issueKey : issueKeys) { - triggerSyncEvent(context.sourceJiraClient().getIssue(issueKey), context); - } - }); - rc.end(); - }); - mi.router().get("/sync/issues/query/full/:project").blockingHandler(rc -> { + mi.router().get("/sync/issues/query/full/:projectGroup").blockingHandler(rc -> { // syncs issue with comments, links etc. - String project = rc.pathParam("project"); + String projectGroup = rc.pathParam("projectGroup"); String query = rc.queryParam("query").getFirst(); - HandlerProjectContext context = contextPerProject.get(project); + HandlerProjectGroupContext context = contextPerProjectGroup.get(projectGroup); if (context == null) { - throw new IllegalArgumentException("Unknown project '%s'".formatted(project)); + throw new IllegalArgumentException("Unknown project '%s'".formatted(projectGroup)); } context.submitTask(() -> syncByQuery(query, context)); rc.end(); }); - mi.router().get("/sync/issues/query/simple/:project").blockingHandler(rc -> { + mi.router().get("/sync/issues/query/simple/:projectGroup").blockingHandler(rc -> { // syncs only assignee/body, without links comments and transitions - String project = rc.pathParam("project"); + String projectGroup = rc.pathParam("projectGroup"); String query = rc.queryParam("query").getFirst(); boolean applyTransitionUpdate = "true".equalsIgnoreCase(rc.queryParam("applyTransitionUpdate").getFirst()); - HandlerProjectContext context = contextPerProject.get(project); + HandlerProjectGroupContext context = contextPerProjectGroup.get(projectGroup); if (context == null) { - throw new IllegalArgumentException("Unknown project '%s'".formatted(project)); + throw new IllegalArgumentException("Unknown project '%s'".formatted(projectGroup)); } context.submitTask(() -> syncByQuery(query, context, @@ -244,46 +198,32 @@ public void registerManagementRoutes(@Observes ManagementInterface mi) { jiraIssue, applyTransitionUpdate)))); rc.end(); }); - mi.router().post("/sync/comments/list").consumes(MediaType.APPLICATION_JSON).blockingHandler(rc -> { - JsonObject request = rc.body().asJsonObject(); - String project = request.getString("project"); - List comments = request.getJsonArray("comments").stream().map(Objects::toString) - .map(JiraComment::new).toList(); - - HandlerProjectContext context = contextPerProject.get(project); - - if (context == null) { - throw new IllegalArgumentException("Unknown project '%s'".formatted(project)); - } - - // TODO: needs an issue key - // can trigger directly as we do not make any REST requests here: - triggerCommentSyncEvents(project, null, comments); - rc.end(); - }); - mi.router().get("/sync/fix-versions/:project").blockingHandler(rc -> { + mi.router().get("/sync/fix-versions/:projectGroup/:project").blockingHandler(rc -> { + String projectGroup = rc.pathParam("projectGroup"); String project = rc.pathParam("project"); - HandlerProjectContext context = contextPerProject.get(project); + HandlerProjectGroupContext context = contextPerProjectGroup.get(projectGroup); if (context == null) { - throw new IllegalArgumentException("Unknown project '%s'".formatted(project)); + throw new IllegalArgumentException("Unknown project '%s'".formatted(projectGroup)); } - context.submitTask(context::refreshFixVersions); + context.submitTask(context.contextForOriginalProjectKey(project)::refreshFixVersions); rc.end(); }); - mi.router().get("/sync/fix-versions/:project/:versionId").blockingHandler(rc -> { + mi.router().get("/sync/fix-versions/:projectGroup/:project/:versionId").blockingHandler(rc -> { + String projectGroup = rc.pathParam("projectGroup"); String project = rc.pathParam("project"); String versionId = rc.pathParam("versionId"); - HandlerProjectContext context = contextPerProject.get(project); + HandlerProjectGroupContext context = contextPerProjectGroup.get(projectGroup); if (context == null) { - throw new IllegalArgumentException("Unknown project '%s'".formatted(project)); + throw new IllegalArgumentException("Unknown project '%s'".formatted(projectGroup)); } - context.fixVersion(context.sourceJiraClient().version(Long.parseLong(versionId)), true); + context.contextForOriginalProjectKey(project) + .fixVersion(context.sourceJiraClient().version(Long.parseLong(versionId)), true); rc.end(); }); } @@ -295,23 +235,25 @@ public void registerManagementRoutes(@Observes ManagementInterface mi) { * the event. It may be that processing will take some time, and it may result * in a timeout on the hook side. * - * @param project - * The project key as defined in the - * {@link JiraConfig.JiraProjectGroup#projects()} + * @param projectGroup + * The project group key as defined in the + * {@link JiraConfig#projectGroup()} * @param event * The body of the event posted by the webhook. * @param triggeredByUser * The ID of the Jira user that triggered the webhook event. */ - public void acknowledge(String project, JiraWebHookEvent event, String triggeredByUser) { + public void acknowledge(String projectGroup, JiraWebHookEvent event, String triggeredByUser) { event.eventType().ifPresentOrElse(eventType -> { - var context = contextPerProject.get(project); + var context = contextPerProjectGroup.get(projectGroup); if (context == null) { FailureCollector failureCollector = FailureCollector.collector(reportingConfig); - failureCollector.critical("Unable to determine handler context for project %s. Was it not configured ?" - .formatted(project)); + failureCollector + .critical("Unable to determine handler context for project group %s. Was it not configured ?" + .formatted(projectGroup)); failureCollector.close(); - throw new ConstraintViolationException("Project " + project + " is not configured.", Set.of()); + throw new ConstraintViolationException("Project group" + projectGroup + " is not configured.", + Set.of()); } if (context.isUserIgnored(triggeredByUser)) { @@ -327,7 +269,7 @@ public void acknowledge(String project, JiraWebHookEvent event, String triggered public void downstreamAcknowledge(String project, JiraActionEvent event) { event.eventType().ifPresentOrElse(eventType -> { - var context = contextPerProject.get(project); + var context = contextPerProjectGroup.get(project); if (context == null) { FailureCollector failureCollector = FailureCollector.collector(reportingConfig); failureCollector.critical("Unable to determine handler context for project %s. Was it not configured ?" @@ -352,13 +294,14 @@ public void syncLastUpdated(String projectGroup) { try (FailureCollector failureCollector = FailureCollector.collector(reportingConfig)) { Log.infof("Starting scheduled sync of issues for the project group %s", projectGroup); + HandlerProjectGroupContext context = contextPerProjectGroup.get(projectGroup); JiraConfig.JiraProjectGroup group = jiraConfig.projectGroup().get(projectGroup); for (String project : group.projects().keySet()) { Log.infof("Generating issues for %s project.", project); - HandlerProjectContext context = contextPerProject.get(project); + HandlerProjectContext projectContext = context.contextForProject(project); String query = "project=%s and updated >= %s ORDER BY key".formatted( - context.project().originalProjectKey(), context.projectGroup().scheduled().timeFilter()); + projectContext.project().originalProjectKey(), context.projectGroup().scheduled().timeFilter()); try { syncByQuery(query, context); } catch (Exception e) { @@ -374,7 +317,7 @@ public void syncLastUpdated(String projectGroup) { @PreDestroy public void finishProcessingAndShutdown() { - for (HandlerProjectContext context : contextPerProject.values()) { + for (HandlerProjectGroupContext context : contextPerProjectGroup.values()) { try { context.close(); } catch (Exception e) { @@ -383,11 +326,11 @@ public void finishProcessingAndShutdown() { } } - private void syncByQuery(String query, HandlerProjectContext context) { + private void syncByQuery(String query, HandlerProjectGroupContext context) { syncByQuery(query, context, jiraIssue -> triggerSyncEvent(jiraIssue, context)); } - private void syncByQuery(String query, HandlerProjectContext context, Consumer action) { + private void syncByQuery(String query, HandlerProjectGroupContext context, Consumer action) { JiraIssues issues = null; int start = 0; int max = 100; @@ -401,7 +344,7 @@ private void syncByQuery(String query, HandlerProjectContext context, Consumer delegate.assign(id, assignee)); } + @Override + public JiraProject project(String projectId) { + return withRetry(() -> delegate.project(projectId)); + } + private static final int RETRIES = 5; private static final Duration WAIT_BETWEEN_RETRIES = Duration.of(2, ChronoUnit.SECONDS); diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraCommentDeleteEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraCommentDeleteEventHandler.java index 43dad3d..0f6406a 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraCommentDeleteEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraCommentDeleteEventHandler.java @@ -2,7 +2,7 @@ import java.util.Optional; -import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectGroupContext; import org.hibernate.infra.replicate.jira.service.jira.client.JiraRestException; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraComment; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraComments; @@ -10,15 +10,16 @@ import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; public class JiraCommentDeleteEventHandler extends JiraCommentEventHandler { - public JiraCommentDeleteEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, Long commentId, - Long issueId) { + public JiraCommentDeleteEventHandler(ReportingConfig reportingConfig, HandlerProjectGroupContext context, + Long commentId, Long issueId) { super(reportingConfig, context, commentId, issueId); } @Override protected void doRun() { JiraIssue issue = context.sourceJiraClient().getIssue(issueId); - String destinationKey = toDestinationKey(issue.key); + String destinationKey = context.contextForOriginalProjectKey(toProjectFromKey(issue.key)) + .toDestinationKey(issue.key); try { JiraComment comment = context.sourceJiraClient().getComment(issueId, objectId); } catch (JiraRestException e) { @@ -39,7 +40,7 @@ protected void doRun() { @Override public String toString() { - return "JiraCommentDeleteEventHandler[" + "issueId=" + issueId + ", objectId=" + objectId + ", project=" - + context.projectName() + ']'; + return "JiraCommentDeleteEventHandler[" + "issueId=" + issueId + ", objectId=" + objectId + ", projectGroup=" + + context.projectGroupName() + ']'; } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraCommentEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraCommentEventHandler.java index b19fa6f..8fd1c27 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraCommentEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraCommentEventHandler.java @@ -3,7 +3,7 @@ import java.util.Optional; import java.util.regex.Pattern; -import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectGroupContext; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraComment; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraComments; import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; @@ -15,7 +15,7 @@ abstract class JiraCommentEventHandler extends JiraEventHandler { protected final Long issueId; - public JiraCommentEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, Long commentId, + public JiraCommentEventHandler(ReportingConfig reportingConfig, HandlerProjectGroupContext context, Long commentId, Long issueId) { super(reportingConfig, context, commentId); this.issueId = issueId; diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraCommentUpsertEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraCommentUpsertEventHandler.java index 17e9563..0e9f334 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraCommentUpsertEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraCommentUpsertEventHandler.java @@ -3,7 +3,7 @@ import java.net.URI; import java.util.Optional; -import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectGroupContext; import org.hibernate.infra.replicate.jira.service.jira.client.JiraRestException; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraComment; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraComments; @@ -12,8 +12,8 @@ public class JiraCommentUpsertEventHandler extends JiraCommentEventHandler { - public JiraCommentUpsertEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, Long commentId, - Long issueId) { + public JiraCommentUpsertEventHandler(ReportingConfig reportingConfig, HandlerProjectGroupContext context, + Long commentId, Long issueId) { super(reportingConfig, context, commentId, issueId); } @@ -22,7 +22,8 @@ protected void doRun() { JiraIssue issue = context.sourceJiraClient().getIssue(issueId); JiraComment comment = context.sourceJiraClient().getComment(issueId, objectId); - String destinationKey = toDestinationKey(issue.key); + String destinationKey = context.contextForOriginalProjectKey(toProjectFromKey(issue.key)) + .toDestinationKey(issue.key); // We are going to assume that the Jira issue was already synced downstream, // and try to find its comments. If the issue is not yet there, then we'll just @@ -75,7 +76,7 @@ private String prepareCommentQuote(JiraIssue issue, JiraComment comment) { @Override public String toString() { - return "JiraCommentUpsertEventHandler[" + "issueId=" + issueId + ", objectId=" + objectId + ", project=" - + context.projectName() + ']'; + return "JiraCommentUpsertEventHandler[" + "issueId=" + issueId + ", objectId=" + objectId + ", projectGroup=" + + context.projectGroupName() + ']'; } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraEventHandler.java index 939c46f..75ce051 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraEventHandler.java @@ -6,10 +6,9 @@ import java.util.Map; import java.util.Optional; import java.util.function.Supplier; -import java.util.regex.Pattern; import org.hibernate.infra.replicate.jira.JiraConfig; -import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectGroupContext; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraComment; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraSimpleObject; @@ -26,14 +25,12 @@ public abstract class JiraEventHandler implements Runnable { protected final Long objectId; protected final FailureCollector failureCollector; - protected final HandlerProjectContext context; - private final Pattern keyToUpdatePattern; + protected final HandlerProjectGroupContext context; - protected JiraEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, Long id) { + protected JiraEventHandler(ReportingConfig reportingConfig, HandlerProjectGroupContext context, Long id) { this.objectId = id; this.failureCollector = FailureCollector.collector(reportingConfig); this.context = context; - this.keyToUpdatePattern = Pattern.compile("^%s-\\d+".formatted(context.project().originalProjectKey())); } protected static URI createJiraIssueUri(JiraIssue sourceIssue) { @@ -203,13 +200,6 @@ protected String truncateContent(String content) { return content; } - protected String toDestinationKey(String key) { - if (keyToUpdatePattern.matcher(key).matches()) { - return "%s-%d".formatted(context.project().projectKey(), JiraIssue.keyToLong(key)); - } - return key; - } - protected String toProjectFromKey(String key) { int index = key.lastIndexOf('-'); return index > 0 ? key.substring(0, index) : null; diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java index 7e4174f..1ed635e 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java @@ -11,6 +11,7 @@ import org.hibernate.infra.replicate.jira.JiraConfig; import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectGroupContext; import org.hibernate.infra.replicate.jira.service.jira.client.JiraRestException; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraFields; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; @@ -27,7 +28,7 @@ abstract class JiraIssueAbstractEventHandler extends JiraEventHandler { private static final Pattern FIX_VERSION_PATTERN = Pattern.compile("Fix_version:.++"); - public JiraIssueAbstractEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, Long id) { + public JiraIssueAbstractEventHandler(ReportingConfig reportingConfig, HandlerProjectGroupContext context, Long id) { super(reportingConfig, context, id); } @@ -47,13 +48,14 @@ protected void applyTransition(JiraIssue sourceIssue, JiraIssue destIssue, Strin jiraTransition -> context.destinationJiraClient().transition(destinationKey, jiraTransition)); } - protected void updateIssueBody(JiraIssue sourceIssue, String destinationKey) { + protected void updateIssueBody(HandlerProjectContext projectContext, JiraIssue sourceIssue, String destinationKey) { JiraIssue destIssue = context.destinationJiraClient().getIssue(destinationKey); - updateIssueBody(sourceIssue, destIssue, destinationKey); + updateIssueBody(projectContext, sourceIssue, destIssue, destinationKey); } - protected void updateIssueBody(JiraIssue sourceIssue, JiraIssue destIssue, String destinationKey) { - JiraIssue issue = issueToCreate(sourceIssue, destIssue); + protected void updateIssueBody(HandlerProjectContext projectContext, JiraIssue sourceIssue, JiraIssue destIssue, + String destinationKey) { + JiraIssue issue = issueToCreate(projectContext, sourceIssue, destIssue); updateIssue(destinationKey, issue, sourceIssue, context.notMappedAssignee()); } @@ -106,7 +108,8 @@ protected JiraRemoteLink remoteSelfLink(JiraIssue sourceIssue) { return link; } - protected JiraIssue issueToCreate(JiraIssue sourceIssue, JiraIssue downstreamIssue) { + protected JiraIssue issueToCreate(HandlerProjectContext projectContext, JiraIssue sourceIssue, + JiraIssue downstreamIssue) { JiraIssue destinationIssue = new JiraIssue(); destinationIssue.fields = new JiraFields(); @@ -124,7 +127,7 @@ protected JiraIssue issueToCreate(JiraIssue sourceIssue, JiraIssue downstreamIss ? priority(sourceIssue.fields.priority.id).map(JiraSimpleObject::new).orElse(null) : null; - destinationIssue.fields.project.id = context.project().projectId(); + destinationIssue.fields.project.id = projectContext.project().projectId(); destinationIssue.fields.issuetype = issueType(sourceIssue.fields.issuetype.id).map(JiraSimpleObject::new) .orElse(null); @@ -150,8 +153,10 @@ protected JiraIssue issueToCreate(JiraIssue sourceIssue, JiraIssue downstreamIss if (!isSubTaskIssue(sourceIssue.fields.issuetype) && sourceIssue.fields.parent != null) { if (isEpicIssue(sourceIssue.fields.parent.fields.issuetype)) { JiraConfig.IssueTypeValueMapping issueTypes = context.projectGroup().issueTypes(); - issueTypes.epicLinkKeyCustomFieldName().ifPresent(epicLinkCustomFieldName -> destinationIssue.fields - .properties().put(epicLinkCustomFieldName, toDestinationKey(sourceIssue.fields.parent.key))); + issueTypes.epicLinkKeyCustomFieldName() + .ifPresent(epicLinkCustomFieldName -> destinationIssue.fields.properties().put( + epicLinkCustomFieldName, + projectContext.toDestinationKey(sourceIssue.fields.parent.key))); } } if (isEpicIssue(sourceIssue.fields.issuetype)) { @@ -168,7 +173,7 @@ protected JiraIssue issueToCreate(JiraIssue sourceIssue, JiraIssue downstreamIss if (sourceIssue.fields.fixVersions != null) { destinationIssue.fields.fixVersions = new ArrayList<>(); for (JiraVersion version : sourceIssue.fields.fixVersions) { - JiraVersion downstream = context.fixVersion(version); + JiraVersion downstream = projectContext.fixVersion(version); if (downstream != null) { destinationIssue.fields.fixVersions.add(downstream); } @@ -178,7 +183,7 @@ protected JiraIssue issueToCreate(JiraIssue sourceIssue, JiraIssue downstreamIss if (sourceIssue.fields.versions != null) { destinationIssue.fields.versions = new ArrayList<>(); for (JiraVersion version : sourceIssue.fields.versions) { - JiraVersion downstream = context.fixVersion(version); + JiraVersion downstream = projectContext.fixVersion(version); if (downstream != null) { destinationIssue.fields.versions.add(downstream); } @@ -237,15 +242,16 @@ protected Optional prepareTransition(String downstreamStatus, Ji .map(JiraTransition::new); } - protected Optional prepareParentLink(String destinationKey, JiraIssue sourceIssue) { + protected Optional prepareParentLink(HandlerProjectContext projectContext, String destinationKey, + JiraIssue sourceIssue) { // we only add a link for sub-tasks (the ones that have a corresponding types). // Issues assigned to an epic, can also have a parent. // but for those we won't add an extra link: if (sourceIssue.fields.parent != null && isSubTaskIssue(sourceIssue.fields.issuetype)) { - String parent = toDestinationKey(sourceIssue.fields.parent.key); + String parent = projectContext.toDestinationKey(sourceIssue.fields.parent.key); // we don't really need it, but as usual we are making sure that the issue is // available downstream: - context.createNextPlaceholderBatch(parent); + projectContext.createNextPlaceholderBatch(parent); JiraIssueLink link = new JiraIssueLink(); link.type.id = context.projectGroup().issueLinkTypes().parentLinkType(); // "name": "Depend", diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueDeleteEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueDeleteEventHandler.java index 02c3bdf..320d5e0 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueDeleteEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueDeleteEventHandler.java @@ -7,7 +7,7 @@ import java.util.Optional; import java.util.Set; -import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectGroupContext; import org.hibernate.infra.replicate.jira.service.jira.client.JiraRestException; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraFields; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; @@ -18,7 +18,7 @@ public class JiraIssueDeleteEventHandler extends JiraIssueAbstractEventHandler { private final String key; - public JiraIssueDeleteEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, Long id, + public JiraIssueDeleteEventHandler(ReportingConfig reportingConfig, HandlerProjectGroupContext context, Long id, String key) { super(reportingConfig, context, id); this.key = key; @@ -52,7 +52,7 @@ protected void doRun() { private void handleDeletedMovedIssue(String type) { try { - String destinationKey = toDestinationKey(key); + String destinationKey = context.contextForOriginalProjectKey(toProjectFromKey(key)).toDestinationKey(key); JiraIssue issue = context.destinationJiraClient().getIssue(destinationKey); JiraIssue updated = new JiraIssue(); @@ -99,7 +99,7 @@ private Optional prepareTransition(JiraIssue issue) { @Override public String toString() { - return "JiraIssueDeleteEventHandler[" + "key='" + key + '\'' + ", objectId=" + objectId + ", project=" - + context.projectName() + ']'; + return "JiraIssueDeleteEventHandler[" + "key='" + key + '\'' + ", objectId=" + objectId + ", projectGroup=" + + context.projectGroupName() + ']'; } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueInternalAbstractEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueInternalAbstractEventHandler.java index 99fbd6f..fb4d3bb 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueInternalAbstractEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueInternalAbstractEventHandler.java @@ -1,6 +1,7 @@ package org.hibernate.infra.replicate.jira.service.jira.handler; import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectGroupContext; import org.hibernate.infra.replicate.jira.service.jira.client.JiraRestException; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; @@ -9,7 +10,7 @@ abstract class JiraIssueInternalAbstractEventHandler extends JiraIssueAbstractEv private final JiraIssue sourceIssue; - protected JiraIssueInternalAbstractEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, + protected JiraIssueInternalAbstractEventHandler(ReportingConfig reportingConfig, HandlerProjectGroupContext context, JiraIssue issue) { super(reportingConfig, context, issue.id); this.sourceIssue = issue; @@ -19,20 +20,22 @@ protected JiraIssueInternalAbstractEventHandler(ReportingConfig reportingConfig, protected final void doRun() { // NOTE: we do not look up the source issue as we've already queried for it // before creating this handler: - String destinationKey = toDestinationKey(sourceIssue.key); + HandlerProjectContext projectContext = context.contextForOriginalProjectKey(sourceIssue.fields.project.key); + String destinationKey = projectContext.toDestinationKey(sourceIssue.key); // We don't really need one, but doing so means that we will create the // placeholder for it if the issue wasn't already present in the destination // Jira instance - context.createNextPlaceholderBatch(destinationKey); + projectContext.createNextPlaceholderBatch(destinationKey); try { - updateAction(destinationKey, sourceIssue); + updateAction(projectContext, destinationKey, sourceIssue); } catch (JiraRestException e) { failureCollector .critical("Unable to update destination issue %s: %s".formatted(destinationKey, e.getMessage()), e); } } - protected abstract void updateAction(String destinationKey, JiraIssue sourceIssue); + protected abstract void updateAction(HandlerProjectContext projectContext, String destinationKey, + JiraIssue sourceIssue); } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueLinkDeleteEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueLinkDeleteEventHandler.java index ea56539..71e809a 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueLinkDeleteEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueLinkDeleteEventHandler.java @@ -1,6 +1,6 @@ package org.hibernate.infra.replicate.jira.service.jira.handler; -import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectGroupContext; import org.hibernate.infra.replicate.jira.service.jira.client.JiraRestException; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssueLink; @@ -11,7 +11,8 @@ public class JiraIssueLinkDeleteEventHandler extends JiraEventHandler { private final Long destinationIssueId; private final String issueLinkTypeId; - public JiraIssueLinkDeleteEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, Long id, + + public JiraIssueLinkDeleteEventHandler(ReportingConfig reportingConfig, HandlerProjectGroupContext context, Long id, Long sourceIssueId, Long destinationIssueId, String issueLinkTypeId) { super(reportingConfig, context, id); this.sourceIssueId = sourceIssueId; @@ -46,8 +47,10 @@ protected void doRun() { } // make sure that both sides of the link exist: - String outwardIssue = toDestinationKey(sourceIssue.key); - String inwardIssue = toDestinationKey(destinationIssue.key); + String outwardIssue = context.contextForOriginalProjectKey(toProjectFromKey(sourceIssue.key)) + .toDestinationKey(sourceIssue.key); + String inwardIssue = context.contextForOriginalProjectKey(toProjectFromKey(destinationIssue.key)) + .toDestinationKey(destinationIssue.key); // we will let it fail if one issue does not exist as that would mean that the // link is also not there: context.destinationJiraClient().getIssue(outwardIssue); @@ -68,7 +71,7 @@ protected void doRun() { @Override public String toString() { return "JiraIssueLinkDeleteEventHandler[" + "sourceIssueId=" + sourceIssueId + ", destinationIssueId=" - + destinationIssueId + ", issueLinkTypeId='" + issueLinkTypeId + '\'' + ", project=" - + context.projectName() + ']'; + + destinationIssueId + ", issueLinkTypeId='" + issueLinkTypeId + '\'' + ", projectGroup=" + + context.projectGroupName() + ']'; } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueLinkUpsertEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueLinkUpsertEventHandler.java index a4de55f..ae07118 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueLinkUpsertEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueLinkUpsertEventHandler.java @@ -4,6 +4,7 @@ import java.util.Optional; import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectGroupContext; import org.hibernate.infra.replicate.jira.service.jira.client.JiraRestException; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssueLink; @@ -14,7 +15,8 @@ public class JiraIssueLinkUpsertEventHandler extends JiraEventHandler { - public JiraIssueLinkUpsertEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, Long id) { + public JiraIssueLinkUpsertEventHandler(ReportingConfig reportingConfig, HandlerProjectGroupContext context, + Long id) { super(reportingConfig, context, id); } @@ -29,22 +31,24 @@ protected void doRun() { return; } - // make sure that both sides of the link exist: - String outwardIssue = toDestinationKey(sourceLink.outwardIssue.key); - String inwardIssue = toDestinationKey(sourceLink.inwardIssue.key); + Optional outwardContextHolder = context + .findContextForOriginalProjectKey(toProjectFromKey(sourceLink.outwardIssue.key)); + Optional inwardContextHolder = context + .findContextForOriginalProjectKey(toProjectFromKey(sourceLink.inwardIssue.key)); - Optional outwardContext = context - .contextForProjectInSameGroup(toProjectFromKey(outwardIssue)); - - Optional inwardContext = context - .contextForProjectInSameGroup(toProjectFromKey(inwardIssue)); - if (inwardContext.isPresent() && outwardContext.isPresent()) { + if (inwardContextHolder.isPresent() && outwardContextHolder.isPresent()) { // means we want to create a simple issue between two projects in the same // project group // so it'll be a regular issue link: - inwardContext.get().createNextPlaceholderBatch(outwardIssue); - outwardContext.get().createNextPlaceholderBatch(inwardIssue); + HandlerProjectContext inwardContext = inwardContextHolder.get(); + HandlerProjectContext outwardContext = outwardContextHolder.get(); + + // make sure that both sides of the link exist: + String outwardIssue = outwardContext.toDestinationKey(sourceLink.outwardIssue.key); + String inwardIssue = inwardContext.toDestinationKey(sourceLink.inwardIssue.key); + inwardContext.createNextPlaceholderBatch(outwardIssue); + outwardContext.createNextPlaceholderBatch(inwardIssue); JiraIssue issue = context.destinationJiraClient().getIssue(inwardIssue); if (issue.fields.issuelinks != null) { @@ -63,10 +67,12 @@ protected void doRun() { toCreate.inwardIssue.key = inwardIssue; toCreate.outwardIssue.key = outwardIssue; context.destinationJiraClient().upsertIssueLink(toCreate); - } else if (outwardContext.isPresent()) { - createAsRemoteLink(sourceLink, inwardIssue, sourceLink.inwardIssue.id, outwardIssue); - } else if (inwardContext.isPresent()) { - createAsRemoteLink(sourceLink, outwardIssue, sourceLink.outwardIssue.id, inwardIssue); + } else if (outwardContextHolder.isPresent()) { + createAsRemoteLink(sourceLink, sourceLink.inwardIssue.key, sourceLink.inwardIssue.id, + outwardContextHolder.get().toDestinationKey(sourceLink.outwardIssue.key)); + } else if (inwardContextHolder.isPresent()) { + createAsRemoteLink(sourceLink, sourceLink.outwardIssue.key, sourceLink.outwardIssue.id, + inwardContextHolder.get().toDestinationKey(sourceLink.inwardIssue.key)); } else { failureCollector.warning("Couldn't find a suitable way to process the issue link for %s".formatted(this)); } @@ -91,6 +97,7 @@ private void createAsRemoteLink(JiraIssueLink sourceLink, String linkedIssueKey, @Override public String toString() { - return "JiraIssueLinkUpsertEventHandler[" + "objectId=" + objectId + ", project=" + context.projectName() + ']'; + return "JiraIssueLinkUpsertEventHandler[" + "objectId=" + objectId + ", projectGroup=" + + context.projectGroupName() + ']'; } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueSimpleUpsertEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueSimpleUpsertEventHandler.java index cd58bd4..ba01d85 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueSimpleUpsertEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueSimpleUpsertEventHandler.java @@ -1,6 +1,7 @@ package org.hibernate.infra.replicate.jira.service.jira.handler; import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectGroupContext; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; @@ -8,21 +9,21 @@ public class JiraIssueSimpleUpsertEventHandler extends JiraIssueInternalAbstract private final boolean applyTransitionUpdate; - public JiraIssueSimpleUpsertEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, + public JiraIssueSimpleUpsertEventHandler(ReportingConfig reportingConfig, HandlerProjectGroupContext context, JiraIssue issue) { this(reportingConfig, context, issue, false); } - public JiraIssueSimpleUpsertEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, + public JiraIssueSimpleUpsertEventHandler(ReportingConfig reportingConfig, HandlerProjectGroupContext context, JiraIssue issue, boolean applyTransitionUpdate) { super(reportingConfig, context, issue); this.applyTransitionUpdate = applyTransitionUpdate; } @Override - protected void updateAction(String destinationKey, JiraIssue sourceIssue) { + protected void updateAction(HandlerProjectContext projectContext, String destinationKey, JiraIssue sourceIssue) { JiraIssue destIssue = context.destinationJiraClient().getIssue(destinationKey); - updateIssueBody(sourceIssue, destIssue, destinationKey); + updateIssueBody(projectContext, sourceIssue, destIssue, destinationKey); if (applyTransitionUpdate) { applyTransition(sourceIssue, destIssue, destinationKey); } @@ -30,7 +31,7 @@ protected void updateAction(String destinationKey, JiraIssue sourceIssue) { @Override public String toString() { - return "JiraIssueSimpleUpsertEventHandler[" + "objectId=" + objectId + ", project=" + context.projectName() - + ']'; + return "JiraIssueSimpleUpsertEventHandler[" + "objectId=" + objectId + ", projectGroup=" + + context.projectGroupName() + ']'; } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueTransitionOnlyEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueTransitionOnlyEventHandler.java index 6eb4e8f..9f3ecf2 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueTransitionOnlyEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueTransitionOnlyEventHandler.java @@ -1,24 +1,25 @@ package org.hibernate.infra.replicate.jira.service.jira.handler; import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectGroupContext; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; public class JiraIssueTransitionOnlyEventHandler extends JiraIssueInternalAbstractEventHandler { - public JiraIssueTransitionOnlyEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, + public JiraIssueTransitionOnlyEventHandler(ReportingConfig reportingConfig, HandlerProjectGroupContext context, JiraIssue issue) { super(reportingConfig, context, issue); } @Override - protected void updateAction(String destinationKey, JiraIssue sourceIssue) { + protected void updateAction(HandlerProjectContext projectContext, String destinationKey, JiraIssue sourceIssue) { applyTransition(sourceIssue, destinationKey); } @Override public String toString() { - return "JiraIssueTransitionOnlyEventHandler[" + "objectId=" + objectId + ", project=" + context.projectName() - + ']'; + return "JiraIssueTransitionOnlyEventHandler[" + "objectId=" + objectId + ", projectGroup=" + + context.projectGroupName() + ']'; } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueUpsertEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueUpsertEventHandler.java index 05cba52..68282a2 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueUpsertEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueUpsertEventHandler.java @@ -1,13 +1,14 @@ package org.hibernate.infra.replicate.jira.service.jira.handler; import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectGroupContext; import org.hibernate.infra.replicate.jira.service.jira.client.JiraRestException; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; public class JiraIssueUpsertEventHandler extends JiraIssueAbstractEventHandler { - public JiraIssueUpsertEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, Long id) { + public JiraIssueUpsertEventHandler(ReportingConfig reportingConfig, HandlerProjectGroupContext context, Long id) { super(reportingConfig, context, id); } @@ -22,16 +23,18 @@ protected void doRun() { return; } - String destinationKey = toDestinationKey(sourceIssue.key); + HandlerProjectContext projectContext = context.contextForOriginalProjectKey(sourceIssue.fields.project.key); + + String destinationKey = projectContext.toDestinationKey(sourceIssue.key); // We don't really need one, but doing so means that we will create the // placeholder for it // if the issue wasn't already present in the destination Jira instance - context.createNextPlaceholderBatch(destinationKey); + projectContext.createNextPlaceholderBatch(destinationKey); try { JiraIssue destIssue = context.destinationJiraClient().getIssue(destinationKey); - updateIssueBody(sourceIssue, destIssue, destinationKey); + updateIssueBody(projectContext, sourceIssue, destIssue, destinationKey); // remote "aka web" links cannot be added in the same request and are also not // returned as part of the issue API. // We "upsert" the remote link pointing to the "original/source" issue that @@ -41,7 +44,8 @@ protected void doRun() { applyTransition(sourceIssue, destIssue, destinationKey); // and then we want to add a link to a parent, if the issue was actually a // sub-task which we've created as a task: - prepareParentLink(destinationKey, sourceIssue).ifPresent(context.destinationJiraClient()::upsertIssueLink); + prepareParentLink(projectContext, destinationKey, sourceIssue) + .ifPresent(context.destinationJiraClient()::upsertIssueLink); } catch (JiraRestException e) { failureCollector .critical("Unable to update destination issue %s: %s".formatted(destinationKey, e.getMessage()), e); @@ -50,6 +54,7 @@ protected void doRun() { @Override public String toString() { - return "JiraIssueUpsertEventHandler[" + "objectId=" + objectId + ", project=" + context.projectName() + ']'; + return "JiraIssueUpsertEventHandler[" + "objectId=" + objectId + ", projectGroup=" + context.projectGroupName() + + ']'; } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraVersionUpsertEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraVersionUpsertEventHandler.java index 4a4889c..66cce65 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraVersionUpsertEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraVersionUpsertEventHandler.java @@ -1,19 +1,21 @@ package org.hibernate.infra.replicate.jira.service.jira.handler; -import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectGroupContext; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraProject; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; public class JiraVersionUpsertEventHandler extends JiraEventHandler { - public JiraVersionUpsertEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, Long id) { + public JiraVersionUpsertEventHandler(ReportingConfig reportingConfig, HandlerProjectGroupContext context, Long id) { super(reportingConfig, context, id); } @Override protected void doRun() { JiraVersion version = context.sourceJiraClient().version(objectId); - context.fixVersion(version, true); + JiraProject project = context.sourceJiraClient().project(version.projectId); + context.findContextForOriginalProjectKey(project.key).get().fixVersion(version, true); } @Override diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAbstractVersionActionEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAbstractVersionActionEventHandler.java index bdb461e..23e84a2 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAbstractVersionActionEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAbstractVersionActionEventHandler.java @@ -3,7 +3,7 @@ import java.util.ArrayList; import java.util.List; -import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectGroupContext; import org.hibernate.infra.replicate.jira.service.jira.model.action.JiraActionEvent; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraFields; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; @@ -12,7 +12,7 @@ abstract class JiraAbstractVersionActionEventHandler extends JiraActionEventHandler { - public JiraAbstractVersionActionEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, + public JiraAbstractVersionActionEventHandler(ReportingConfig reportingConfig, HandlerProjectGroupContext context, JiraActionEvent event) { super(reportingConfig, context, event); } @@ -40,7 +40,7 @@ protected void doRun() { setVersionList(updated, versions); - context.sourceJiraClient().update(toSourceKey(event.key), updated); + context.sourceJiraClient().update(context.contextForProject(event.projectKey).toSourceKey(event.key), updated); } protected abstract void setVersionList(JiraIssue issue, List versions); @@ -49,6 +49,7 @@ protected void doRun() { @Override public String toString() { - return this.getClass().getSimpleName() + "[" + "event=" + event + ", project=" + context.projectName() + ']'; + return this.getClass().getSimpleName() + "[" + "event=" + event + ", projectGroup=" + context.projectGroupName() + + ']'; } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraActionEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraActionEventHandler.java index 7834e43..a0fbadf 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraActionEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraActionEventHandler.java @@ -1,8 +1,7 @@ package org.hibernate.infra.replicate.jira.service.jira.handler.action; -import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectGroupContext; import org.hibernate.infra.replicate.jira.service.jira.model.action.JiraActionEvent; -import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; import org.hibernate.infra.replicate.jira.service.reporting.FailureCollector; import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; @@ -12,9 +11,9 @@ public abstract class JiraActionEventHandler implements Runnable { protected final JiraActionEvent event; protected final FailureCollector failureCollector; - protected final HandlerProjectContext context; + protected final HandlerProjectGroupContext context; - protected JiraActionEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, + protected JiraActionEventHandler(ReportingConfig reportingConfig, HandlerProjectGroupContext context, JiraActionEvent event) { this.event = event; this.failureCollector = FailureCollector.collector(reportingConfig); @@ -38,10 +37,6 @@ public final void run() { } } - protected String toSourceKey(String key) { - return "%s-%d".formatted(context.project().originalProjectKey(), JiraIssue.keyToLong(key)); - } - protected abstract void doRun(); public abstract String toString(); diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAffectsVersionActionEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAffectsVersionActionEventHandler.java index 32585c2..2d26d82 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAffectsVersionActionEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAffectsVersionActionEventHandler.java @@ -2,7 +2,7 @@ import java.util.List; -import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectGroupContext; import org.hibernate.infra.replicate.jira.service.jira.model.action.JiraActionEvent; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; @@ -10,7 +10,7 @@ public class JiraAffectsVersionActionEventHandler extends JiraAbstractVersionActionEventHandler { - public JiraAffectsVersionActionEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, + public JiraAffectsVersionActionEventHandler(ReportingConfig reportingConfig, HandlerProjectGroupContext context, JiraActionEvent event) { super(reportingConfig, context, event); } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAssigneeActionEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAssigneeActionEventHandler.java index e52345d..c83fa4b 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAssigneeActionEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraAssigneeActionEventHandler.java @@ -1,6 +1,6 @@ package org.hibernate.infra.replicate.jira.service.jira.handler.action; -import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectGroupContext; import org.hibernate.infra.replicate.jira.service.jira.model.action.JiraActionEvent; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser; @@ -8,7 +8,7 @@ public class JiraAssigneeActionEventHandler extends JiraActionEventHandler { - public JiraAssigneeActionEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, + public JiraAssigneeActionEventHandler(ReportingConfig reportingConfig, HandlerProjectGroupContext context, JiraActionEvent event) { super(reportingConfig, context, event); } @@ -29,12 +29,13 @@ protected void doRun() { user = new JiraUser("-1"); } if (user != null) { - context.sourceJiraClient().assign(toSourceKey(event.key), user); + context.sourceJiraClient().assign(context.contextForProject(event.projectKey).toSourceKey(event.key), user); } } @Override public String toString() { - return "JiraAssigneeActionEventHandler[" + "event=" + event + ", project=" + context.projectName() + ']'; + return "JiraAssigneeActionEventHandler[" + "event=" + event + ", projectGroup=" + context.projectGroupName() + + ']'; } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraFixVersionActionEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraFixVersionActionEventHandler.java index 7eed14c..a2c3ea7 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraFixVersionActionEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraFixVersionActionEventHandler.java @@ -2,7 +2,7 @@ import java.util.List; -import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectGroupContext; import org.hibernate.infra.replicate.jira.service.jira.model.action.JiraActionEvent; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; @@ -10,7 +10,7 @@ public class JiraFixVersionActionEventHandler extends JiraAbstractVersionActionEventHandler { - public JiraFixVersionActionEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, + public JiraFixVersionActionEventHandler(ReportingConfig reportingConfig, HandlerProjectGroupContext context, JiraActionEvent event) { super(reportingConfig, context, event); } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraTransitionActionEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraTransitionActionEventHandler.java index c05f936..0b6d0a3 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraTransitionActionEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/action/JiraTransitionActionEventHandler.java @@ -5,7 +5,7 @@ import java.util.Set; import java.util.function.Supplier; -import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectGroupContext; import org.hibernate.infra.replicate.jira.service.jira.handler.JiraStaticFieldMappingCache; import org.hibernate.infra.replicate.jira.service.jira.model.action.JiraActionEvent; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; @@ -15,14 +15,14 @@ public class JiraTransitionActionEventHandler extends JiraActionEventHandler { - public JiraTransitionActionEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, + public JiraTransitionActionEventHandler(ReportingConfig reportingConfig, HandlerProjectGroupContext context, JiraActionEvent event) { super(reportingConfig, context, event); } @Override protected void doRun() { - String sourceKey = toSourceKey(event.key); + String sourceKey = context.contextForProject(event.projectKey).toSourceKey(event.key); JiraIssue issue = context.destinationJiraClient().getIssue(event.key); JiraIssue sourceIssue = context.sourceJiraClient().getIssue(sourceKey); @@ -53,6 +53,7 @@ protected Optional statusToTransition(String from, String to, Supplier handlers(ReportingConfig reportingConfig, JiraActionEvent event, - HandlerProjectContext context) { + HandlerProjectGroupContext context) { if (event.assignee == null || event.key == null) { throw new IllegalStateException( "Trying to handle an issue event but issue id is null: %s".formatted(event)); @@ -28,21 +28,21 @@ public Collection handlers(ReportingConfig reportingConfig, JiraAction ISSUE_TRANSITIONED("jira:issue_update_status") { @Override public Collection handlers(ReportingConfig reportingConfig, JiraActionEvent event, - HandlerProjectContext context) { + HandlerProjectGroupContext context) { return List.of(new JiraTransitionActionEventHandler(reportingConfig, context, event)); } }, FIX_VERSION_CHANGED("jira:issue_update_fixversion") { @Override public Collection handlers(ReportingConfig reportingConfig, JiraActionEvent event, - HandlerProjectContext context) { + HandlerProjectGroupContext context) { return List.of(new JiraFixVersionActionEventHandler(reportingConfig, context, event)); } }, AFFECTS_VERSION_CHANGED("jira:issue_update_version") { @Override public Collection handlers(ReportingConfig reportingConfig, JiraActionEvent event, - HandlerProjectContext context) { + HandlerProjectGroupContext context) { return List.of(new JiraAffectsVersionActionEventHandler(reportingConfig, context, event)); } }; @@ -70,5 +70,5 @@ public static Optional of(String webhookEvent) { } public abstract Collection handlers(ReportingConfig reportingConfig, JiraActionEvent event, - HandlerProjectContext context); + HandlerProjectGroupContext context); } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/hook/JiraWebhookEventType.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/hook/JiraWebhookEventType.java index 0d7182a..3437f63 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/hook/JiraWebhookEventType.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/hook/JiraWebhookEventType.java @@ -5,7 +5,7 @@ import java.util.Locale; import java.util.Optional; -import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; +import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectGroupContext; import org.hibernate.infra.replicate.jira.service.jira.handler.JiraCommentDeleteEventHandler; import org.hibernate.infra.replicate.jira.service.jira.handler.JiraCommentUpsertEventHandler; import org.hibernate.infra.replicate.jira.service.jira.handler.JiraIssueDeleteEventHandler; @@ -19,7 +19,7 @@ public enum JiraWebhookEventType { ISSUE_CREATED("jira:issue_created") { @Override public Collection handlers(ReportingConfig reportingConfig, JiraWebHookEvent event, - HandlerProjectContext context) { + HandlerProjectGroupContext context) { if (event.issue == null || event.issue.id == null) { throw new IllegalStateException( "Trying to handle an issue event but issue id is null: %s".formatted(event)); @@ -30,7 +30,7 @@ public Collection handlers(ReportingConfig reportingConfig, JiraWebHoo ISSUE_UPDATED("jira:issue_updated") { @Override public Collection handlers(ReportingConfig reportingConfig, JiraWebHookEvent event, - HandlerProjectContext context) { + HandlerProjectGroupContext context) { if (event.issue == null || event.issue.id == null) { throw new IllegalStateException( "Trying to handle an issue event but issue id is null: %s".formatted(event)); @@ -41,7 +41,7 @@ public Collection handlers(ReportingConfig reportingConfig, JiraWebHoo ISSUE_DELETED("jira:issue_deleted") { @Override public Collection handlers(ReportingConfig reportingConfig, JiraWebHookEvent event, - HandlerProjectContext context) { + HandlerProjectGroupContext context) { if (event.issue == null || event.issue.id == null) { throw new IllegalStateException( "Trying to handle an issue event but issue id is null: %s".formatted(event)); @@ -52,7 +52,7 @@ public Collection handlers(ReportingConfig reportingConfig, JiraWebHoo ISSUELINK_CREATED("issuelink_created") { @Override public Collection handlers(ReportingConfig reportingConfig, JiraWebHookEvent event, - HandlerProjectContext context) { + HandlerProjectGroupContext context) { if (event.issueLink == null || event.issueLink.id == null) { throw new IllegalStateException( "Trying to handle an issue link event but source issue id is null: %s".formatted(event)); @@ -65,7 +65,7 @@ public Collection handlers(ReportingConfig reportingConfig, JiraWebHoo ISSUELINK_DELETED("issuelink_deleted") { @Override public Collection handlers(ReportingConfig reportingConfig, JiraWebHookEvent event, - HandlerProjectContext context) { + HandlerProjectGroupContext context) { if (event.issueLink == null || event.issueLink.id == null) { throw new IllegalStateException( "Trying to handle an issue link event but source issue id is null: %s".formatted(event)); @@ -80,7 +80,7 @@ public Collection handlers(ReportingConfig reportingConfig, JiraWebHoo COMMENT_CREATED("comment_created") { @Override public Collection handlers(ReportingConfig reportingConfig, JiraWebHookEvent event, - HandlerProjectContext context) { + HandlerProjectGroupContext context) { if (event.comment == null || event.comment.id == null) { throw new IllegalStateException( "Trying to handle a comment event but comment id is null: %s".formatted(event)); @@ -96,7 +96,7 @@ public Collection handlers(ReportingConfig reportingConfig, JiraWebHoo COMMENT_UPDATED("comment_updated") { @Override public Collection handlers(ReportingConfig reportingConfig, JiraWebHookEvent event, - HandlerProjectContext context) { + HandlerProjectGroupContext context) { if (event.comment == null || event.comment.id == null) { throw new IllegalStateException( "Trying to handle a comment event but comment id is null: %s".formatted(event)); @@ -112,7 +112,7 @@ public Collection handlers(ReportingConfig reportingConfig, JiraWebHoo COMMENT_DELETED("comment_deleted") { @Override public Collection handlers(ReportingConfig reportingConfig, JiraWebHookEvent event, - HandlerProjectContext context) { + HandlerProjectGroupContext context) { if (event.comment == null || event.comment.id == null) { throw new IllegalStateException( "Trying to handle a comment event but comment id is null: %s".formatted(event)); @@ -124,7 +124,7 @@ public Collection handlers(ReportingConfig reportingConfig, JiraWebHoo VERSION_CREATED("jira:version_created") { @Override public Collection handlers(ReportingConfig reportingConfig, JiraWebHookEvent event, - HandlerProjectContext context) { + HandlerProjectGroupContext context) { if (event.version == null || event.version.id == null) { throw new IllegalStateException( "Trying to handle a version event but version id is null: %s".formatted(event)); @@ -135,7 +135,7 @@ public Collection handlers(ReportingConfig reportingConfig, JiraWebHoo VERSION_UPDATED("jira:version_updated") { @Override public Collection handlers(ReportingConfig reportingConfig, JiraWebHookEvent event, - HandlerProjectContext context) { + HandlerProjectGroupContext context) { if (event.version == null || event.version.id == null) { throw new IllegalStateException( "Trying to handle a version event but version id is null: %s".formatted(event)); @@ -167,5 +167,5 @@ public static Optional of(String webhookEvent) { } public abstract Collection handlers(ReportingConfig reportingConfig, JiraWebHookEvent event, - HandlerProjectContext context); + HandlerProjectGroupContext context); } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java index f2b9f35..d9cc16c 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java @@ -13,7 +13,7 @@ public class JiraFields extends JiraBaseObject { public String description; public JiraSimpleObject priority = new JiraSimpleObject(); public JiraSimpleObject issuetype = new JiraSimpleObject(); - public JiraSimpleObject project = new JiraSimpleObject(); + public JiraProject project = new JiraProject(); public JiraSimpleObject status; public List labels; diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraProject.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraProject.java new file mode 100644 index 0000000..a6e23ea --- /dev/null +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraProject.java @@ -0,0 +1,24 @@ +package org.hibernate.infra.replicate.jira.service.jira.model.rest; + +import java.net.URI; + +import org.hibernate.infra.replicate.jira.service.jira.model.JiraBaseObject; + +public class JiraProject extends JiraBaseObject { + public String id; + public String key; + public URI self; + + public JiraProject() { + } + + public JiraProject(String id) { + this.id = id; + } + + @Override + public String toString() { + return "JiraProject{" + "id=" + id + ", key='" + key + '\'' + ", self=" + self + "otherProperties=" + + properties() + '}'; + } +} diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProjectGroup.java b/src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProjectGroup.java new file mode 100644 index 0000000..890dc87 --- /dev/null +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProjectGroup.java @@ -0,0 +1,28 @@ +package org.hibernate.infra.replicate.jira.service.validation; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +@Documented +@Constraint(validatedBy = {ConfiguredProjectGroupValidator.class}) +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) +@Retention(RUNTIME) +public @interface ConfiguredProjectGroup { + String message() default "The ${projectGroup} project group is not configured"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProjectGroupValidator.java b/src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProjectGroupValidator.java new file mode 100644 index 0000000..005ec62 --- /dev/null +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProjectGroupValidator.java @@ -0,0 +1,32 @@ +package org.hibernate.infra.replicate.jira.service.validation; + +import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorContext; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class ConfiguredProjectGroupValidator implements ConstraintValidator { + + @Inject + Instance configuredProjectsService; + + @Override + public void initialize(ConfiguredProjectGroup constraintAnnotation) { + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return true; + } + if (configuredProjectsService.get().isProjectGroup(value)) { + return true; + } + + context.unwrap(HibernateConstraintValidatorContext.class).addExpressionVariable("projectGroup", value); + + return false; + } +} diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProjectsService.java b/src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProjectsService.java index e9f84fe..1d23cf0 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProjectsService.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/validation/ConfiguredProjectsService.java @@ -13,12 +13,15 @@ public class ConfiguredProjectsService { private final Set upstreamProjects; private final Set downstreamProjects; + private final Set projectGroups; public ConfiguredProjectsService(JiraConfig jiraConfig) { Set down = new HashSet<>(); Set up = new HashSet<>(); - for (JiraConfig.JiraProjectGroup group : jiraConfig.projectGroup().values()) { - for (JiraConfig.JiraProject project : group.projects().values()) { + Set groups = new HashSet<>(); + for (var group : jiraConfig.projectGroup().entrySet()) { + groups.add(group.getKey()); + for (JiraConfig.JiraProject project : group.getValue().projects().values()) { up.add(project.originalProjectKey()); down.add(project.projectKey()); } @@ -26,6 +29,7 @@ public ConfiguredProjectsService(JiraConfig jiraConfig) { upstreamProjects = Collections.unmodifiableSet(up); downstreamProjects = Collections.unmodifiableSet(down); + projectGroups = Collections.unmodifiableSet(groups); } public boolean isUpstreamProject(String projectName) { @@ -35,4 +39,8 @@ public boolean isUpstreamProject(String projectName) { public boolean isDownstreamProject(String projectName) { return downstreamProjects.contains(projectName); } + + public boolean isProjectGroup(String value) { + return projectGroups.contains(value); + } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/validation/RequestSignatureFilter.java b/src/main/java/org/hibernate/infra/replicate/jira/service/validation/RequestSignatureFilter.java index 05e4220..ff4efe8 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/validation/RequestSignatureFilter.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/validation/RequestSignatureFilter.java @@ -31,8 +31,9 @@ @ApplicationScoped public class RequestSignatureFilter { - private static final Pattern PATH_UPSTREAM_WEBHOOK_PATTERN = Pattern.compile("/jira/webhooks/(.+)"); - private static final Pattern PATH_DOWNSTREAM_WEBHOOK_PATTERN = Pattern.compile("/jira/webhooks/mirror/(.+)"); + private static final Pattern PATH_UPSTREAM_WEBHOOK_PATTERN = Pattern.compile("/jira/webhooks/source/(.+)"); + private static final Pattern PATH_DOWNSTREAM_WEBHOOK_PATTERN = Pattern + .compile("/jira/webhooks/mirror/[^\\/]++/(.+)"); private static final BiPredicate ALLOW_ALL = (a, b) -> true; private final Map> upstreamChecks; @@ -42,9 +43,10 @@ public class RequestSignatureFilter { public RequestSignatureFilter(JiraConfig jiraConfig) { Map> up = new HashMap<>(); Map> down = new HashMap<>(); - for (JiraConfig.JiraProjectGroup group : jiraConfig.projectGroup().values()) { - for (var entry : group.projects().entrySet()) { - up.put(entry.getKey(), check(entry.getValue().security())); + for (var group : jiraConfig.projectGroup().entrySet()) { + up.put(group.getKey(), check(group.getValue().security())); + + for (var entry : group.getValue().projects().entrySet()) { down.put(entry.getKey(), check(entry.getValue().downstreamSecurity())); } } @@ -85,8 +87,8 @@ public Response checkSignature(ContainerRequestContext requestContext) throws IO String project = downstream.group(1); check = downstreamChecks.get(project); } else if (upstream.matches()) { - String project = upstream.group(1); - check = upstreamChecks.get(project); + String projectGroup = upstream.group(1); + check = upstreamChecks.get(projectGroup); } if (check != null) { String signature = requestContext.getHeaderString("x-hub-signature"); diff --git a/src/test/java/org/hibernate/infra/replicate/jira/SimpleProjectHookTest.java b/src/test/java/org/hibernate/infra/replicate/jira/SimpleProjectHookTest.java index d432889..a5e6b38 100644 --- a/src/test/java/org/hibernate/infra/replicate/jira/SimpleProjectHookTest.java +++ b/src/test/java/org/hibernate/infra/replicate/jira/SimpleProjectHookTest.java @@ -33,22 +33,23 @@ class SimpleProjectHookTest { @Test void unknown() { - given().when().body(REQUEST_BODY).contentType(ContentType.JSON).post("api/jira/webhooks/NOTAPROJECTKEY").then() - .statusCode(400).body(containsString("The NOTAPROJECTKEY project is not configured")); + given().when().body(REQUEST_BODY).contentType(ContentType.JSON) + .post("api/jira/webhooks/source/NOTAPROJECTGROUPKEY").then().statusCode(400) + .body(containsString("The NOTAPROJECTGROUPKEY project group is not configured")); } @Test void known() { given().when().body(REQUEST_BODY) .header("x-hub-signature", RequestSignatureFilter.sign("not-a-secret", REQUEST_BODY)) - .contentType(ContentType.JSON).post("api/jira/webhooks/JIRATEST1").then().statusCode(200) + .contentType(ContentType.JSON).post("api/jira/webhooks/source/hibernate").then().statusCode(200) .body(is("ack")); } @Test void knownNoHeader() { - given().when().body(REQUEST_BODY).contentType(ContentType.JSON).post("api/jira/webhooks/JIRATEST1").then() - .statusCode(401); + given().when().body(REQUEST_BODY).contentType(ContentType.JSON).post("api/jira/webhooks/source/hibernate") + .then().statusCode(401); } } diff --git a/src/test/java/org/hibernate/infra/replicate/jira/export/ExportProjectTest.java b/src/test/java/org/hibernate/infra/replicate/jira/export/ExportProjectTest.java index 502e50d..e8cfe71 100644 --- a/src/test/java/org/hibernate/infra/replicate/jira/export/ExportProjectTest.java +++ b/src/test/java/org/hibernate/infra/replicate/jira/export/ExportProjectTest.java @@ -124,7 +124,7 @@ void export() throws IOException { row.add(issueTypeMap.getOrDefault(issue.fields.issuetype.name, "")); row.add(statusMap.getOrDefault(issue.fields.status.name, "")); row.add(issue.fields.project.properties().get("key")); - row.add(issue.fields.project.name); + row.add(issue.fields.project.properties().get("name")); Object reporter = issue.fields.reporter == null ? null : projectGroup.users().mapping().getOrDefault(issue.fields.reporter.accountId, null); diff --git a/src/test/java/org/hibernate/infra/replicate/jira/handler/IssueTest.java b/src/test/java/org/hibernate/infra/replicate/jira/handler/IssueTest.java index 1f88ffa..697208e 100644 --- a/src/test/java/org/hibernate/infra/replicate/jira/handler/IssueTest.java +++ b/src/test/java/org/hibernate/infra/replicate/jira/handler/IssueTest.java @@ -4,12 +4,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import java.util.HashMap; -import java.util.Map; - import org.hibernate.infra.replicate.jira.JiraConfig; import org.hibernate.infra.replicate.jira.mock.SampleJiraRestClient; -import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectGroupContext; import org.hibernate.infra.replicate.jira.service.jira.handler.JiraCommentDeleteEventHandler; import org.hibernate.infra.replicate.jira.service.jira.handler.JiraCommentUpsertEventHandler; @@ -45,18 +41,12 @@ class IssueTest { @Inject JiraConfig jiraConfig; - HandlerProjectContext context; + HandlerProjectGroupContext context; @BeforeEach void setUp() { - Map contextMap = new HashMap<>(); - HandlerProjectGroupContext projectGroupContext = new HandlerProjectGroupContext( - jiraConfig.projectGroup().get(PROJECT_GROUP_NAME)); - context = new HandlerProjectContext("JIRATEST1", PROJECT_GROUP_NAME, source, destination, projectGroupContext, - contextMap); - contextMap.put("JIRATEST1", context); - contextMap.put("JIRATEST2", new HandlerProjectContext("JIRATEST2", PROJECT_GROUP_NAME, source, destination, - projectGroupContext, contextMap)); + context = new HandlerProjectGroupContext(PROJECT_GROUP_NAME, jiraConfig.projectGroup().get(PROJECT_GROUP_NAME), + source, destination); } @AfterEach diff --git a/src/test/java/org/hibernate/infra/replicate/jira/mock/SampleJiraRestClient.java b/src/test/java/org/hibernate/infra/replicate/jira/mock/SampleJiraRestClient.java index 428f449..66351f1 100644 --- a/src/test/java/org/hibernate/infra/replicate/jira/mock/SampleJiraRestClient.java +++ b/src/test/java/org/hibernate/infra/replicate/jira/mock/SampleJiraRestClient.java @@ -21,6 +21,7 @@ import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssueResponse; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssueTransition; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssues; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraProject; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraRemoteLink; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraSimpleObject; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTransition; @@ -234,6 +235,14 @@ public void assign(String id, JiraUser assignee) { // ok } + @Override + public JiraProject project(String projectId) { + JiraProject project = new JiraProject(); + project.id = projectId; + project.key = "JIRATEST1"; + return project; + } + private JiraIssueLink sampleIssueLink(Long id) { try { return objectMapper.readValue(""" diff --git a/src/test/java/org/hibernate/infra/replicate/jira/service/validation/RequestSignatureFilterTest.java b/src/test/java/org/hibernate/infra/replicate/jira/service/validation/RequestSignatureFilterTest.java index 95f4af1..87052d3 100644 --- a/src/test/java/org/hibernate/infra/replicate/jira/service/validation/RequestSignatureFilterTest.java +++ b/src/test/java/org/hibernate/infra/replicate/jira/service/validation/RequestSignatureFilterTest.java @@ -24,19 +24,21 @@ void checkSignature() throws Exception { RequestSignatureFilter filter = createFilter(true, "wrong-secret"); assertThat(filter.checkSignature(requestContext("/path-not-matching-the-pattern"))).isNull(); - assertThat(filter.checkSignature(requestContext("/jira/webhooks/UNKNOWN"))).isNull(); - assertThat(filter.checkSignature(requestContext("/jira/webhooks/UNKNOWN", "POST", "smth"))).isNull(); + assertThat(filter.checkSignature(requestContext("/jira/webhooks/source/UNKNOWN"))).isNull(); + assertThat(filter.checkSignature(requestContext("/jira/webhooks/source/UNKNOWN", "POST", "smth"))).isNull(); - Response response = filter.checkSignature(requestContext("/jira/webhooks/PROJECT_KEY", "POST", "smth")); + Response response = filter + .checkSignature(requestContext("/jira/webhooks/source/PROJECT_GROUP_KEY", "POST", "smth")); assertThat(response).isNotNull(); assertThat((String) response.getEntity()).contains("Missing x-hub-signature header"); - response = filter.checkSignature( - requestContext("/jira/webhooks/PROJECT_KEY", "POST", "smth", Map.of("x-hub-signature", "smth-wrong"))); + response = filter.checkSignature(requestContext("/jira/webhooks/source/PROJECT_GROUP_KEY", "POST", "smth", + Map.of("x-hub-signature", "smth-wrong"))); assertThat(response).isNotNull(); assertThat((String) response.getEntity()).contains("Signatures do not match"); - ContainerRequestContext requestContext = requestContext("/jira/webhooks/PROJECT_KEY", "POST", "smth", + ContainerRequestContext requestContext = requestContext("/jira/webhooks/source/PROJECT_GROUP_KEY", "POST", + "smth", Map.of("x-hub-signature", "sha256=beb656dd03b5a81bee94aab4966943bf694929fcd359c45a1947277df2541a5d")); assertThat(filter.checkSignature(requestContext)).isNull(); Mockito.verify(requestContext, Mockito.times(1)).setEntityStream(Mockito.any()); @@ -85,7 +87,7 @@ public Type type() { return Type.SIGNATURE; } }; - Mockito.when(project.security()).thenReturn(value); + Mockito.when(group.security()).thenReturn(value); Mockito.when(project.downstreamSecurity()).thenReturn(value); Mockito.when(group.projects()).thenReturn(Map.of("PROJECT_KEY", project)); diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index ceb359f..0d5596b 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -17,14 +17,12 @@ jira.project-group."hibernate".issue-link-types.default-value=10050 jira.project-group."hibernate".statuses.mapping."to\u0020do"=to do -jira.project-group."hibernate".projects.JIRATEST1.security.secret=not-a-secret +jira.project-group."hibernate".security.secret=not-a-secret +jira.project-group."hibernate".security.enabled=true jira.project-group."hibernate".projects.JIRATEST1.project-id=10323 jira.project-group."hibernate".projects.JIRATEST1.project-key=JIRATEST2 jira.project-group."hibernate".projects.JIRATEST1.original-project-key=JIRATEST1 -jira.project-group."hibernate".projects.JIRATEST1.security.enabled=true -jira.project-group."hibernate".projects.JIRATEST2.security.secret=not-a-secret jira.project-group."hibernate".projects.JIRATEST2.project-id=10324 jira.project-group."hibernate".projects.JIRATEST2.project-key=JIRATEST2 jira.project-group."hibernate".projects.JIRATEST2.original-project-key=JIRATEST2 -jira.project-group."hibernate".projects.JIRATEST2.security.enabled=true