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 1bda3a9..cbd7480 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java @@ -310,10 +310,10 @@ interface StatusesValueMapping extends ValueMapping { Optional deletedResolution(); /** - * @return The id of the transition to apply to get the "Close" transition when - * closing the issue deleted upstream before archiving it. + * @return The status name to transition the issue to when handling an issue + * deleted/moved upstream before archiving it. */ - Optional deletedTransition(); + Optional deletedStatus(); /** * @return A map where the {@code key} is the upstream status name, and the diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClient.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClient.java index 348ab6c..0ff2c63 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClient.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClient.java @@ -15,6 +15,7 @@ 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; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTransitions; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; @@ -145,6 +146,10 @@ JiraIssues find(@QueryParam("jql") String query, @QueryParam("startAt") int star @Path("/issue/{issueKey}/transitions") void transition(@PathParam("issueKey") String issueKey, JiraTransition transition); + @GET + @Path("/issue/{issueKey}/transitions") + JiraTransitions availableTransitions(String issueKey); + @PUT @Path("/issue/{issueKey}/archive") void archive(@PathParam("issueKey") String issueKey); diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClientBuilder.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClientBuilder.java index 1fd871d..ad07d98 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClientBuilder.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/client/JiraRestClientBuilder.java @@ -21,6 +21,7 @@ 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; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTransitions; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; @@ -250,6 +251,11 @@ public void transition(String issueKey, JiraTransition transition) { withRetry(() -> delegate.transition(issueKey, transition)); } + @Override + public JiraTransitions availableTransitions(String issueKey) { + return withRetry(() -> delegate.availableTransitions(issueKey)); + } + @Override public void archive(String issueKey) { withRetry(() -> delegate.archive(issueKey)); 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 5501a7f..23280eb 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 @@ -1,18 +1,22 @@ package org.hibernate.infra.replicate.jira.service.jira.handler; import java.net.URI; +import java.util.Collections; import java.util.HashMap; import java.util.List; 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.model.rest.JiraComment; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssueTransition; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraSimpleObject; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTextContent; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTransitions; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser; import org.hibernate.infra.replicate.jira.service.reporting.FailureCollector; import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; @@ -92,21 +96,29 @@ protected Optional issueType(String sourceId) { }, mappedValues.defaultValue())); } - protected Optional statusToTransition(String sourceId) { - JiraConfig.ValueMapping mappedValues = context.projectGroup().statuses(); - return Optional.ofNullable(JiraStaticFieldMappingCache.status(context.projectGroupName(), sourceId, pk -> { - Map mapping = context.projectGroup().statuses().mapping(); - if (!mapping.isEmpty()) { - return mapping; - } + protected Optional statusToTransition(String from, String to, Supplier> transitionFinder) { + return Optional.ofNullable(JiraStaticFieldMappingCache.status(context.projectGroupName(), + "%s->%s".formatted(from, to), tk -> transitionFinder.get().orElse(null))); + } - // Otherwise we'll try to use REST to get the info and match, but that may not - // necessarily work fine - List source = context.sourceJiraClient().getStatues(); - List destination = context.destinationJiraClient().getStatues(); + protected Optional findRequiredTransitionId(String downstreamStatus, JiraIssue destIssue) { + if (downstreamStatus != null) { + List jiraTransitions = null; + try { + JiraTransitions transitions = context.destinationJiraClient().availableTransitions(destIssue.key); + jiraTransitions = transitions.transitions; + } catch (Exception e) { + failureCollector.warning("Failed to find a transition for %s".formatted(destIssue.key), e); + jiraTransitions = Collections.emptyList(); + } + for (JiraIssueTransition transition : jiraTransitions) { + if (transition.to != null && downstreamStatus.equalsIgnoreCase(transition.to.name)) { + return Optional.of(transition.id); + } + } + } - return createMapping(source, destination); - }, mappedValues.defaultValue())); + return Optional.empty(); } protected Optional linkType(String sourceId) { 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 0f21ea7..9619f66 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 @@ -37,12 +37,12 @@ protected void applyTransition(JiraIssue sourceIssue, String destinationKey) { protected void applyTransition(JiraIssue sourceIssue, JiraIssue destIssue, String destinationKey) { Set statusesToIgnore = context.projectGroup().statuses().ignoreTransitionCondition() - .getOrDefault(sourceIssue.fields.status.name.toLowerCase(Locale.ROOT).replace(' ', '-'), Set.of()); + .getOrDefault(sourceIssue.fields.status.name.toLowerCase(Locale.ROOT), Set.of()); if (statusesToIgnore.contains(destIssue.fields.status.name.toLowerCase(Locale.ROOT))) { // no need to apply the transition :) return; } - prepareTransition(sourceIssue).ifPresent( + prepareTransition(sourceIssue.fields.status, destIssue).ifPresent( jiraTransition -> context.destinationJiraClient().transition(destinationKey, jiraTransition)); } @@ -213,8 +213,18 @@ private JiraUser toUser(String value) { return new JiraUser(context.projectGroup().users().mappedPropertyName(), value); } - private Optional prepareTransition(JiraIssue sourceIssue) { - return statusToTransition(sourceIssue.fields.status.id).map(JiraTransition::new); + protected Optional prepareTransition(JiraSimpleObject sourceStatus, JiraIssue destIssue) { + String downstreamStatus = context.projectGroup().statuses().mapping() + .get(sourceStatus.name.toLowerCase(Locale.ROOT)); + return prepareTransition(downstreamStatus, destIssue); + } + + protected Optional prepareTransition(String downstreamStatus, JiraIssue destIssue) { + return Optional + .ofNullable(statusToTransition(destIssue.fields.status.name, downstreamStatus, + () -> findRequiredTransitionId(downstreamStatus, destIssue)) + .orElseGet(() -> findRequiredTransitionId(downstreamStatus, destIssue).orElse(null))) + .map(JiraTransition::new); } protected Optional prepareParentLink(String destinationKey, JiraIssue sourceIssue) { 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 d598177..02c3bdf 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 @@ -15,7 +15,7 @@ import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTransition; import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig; -public class JiraIssueDeleteEventHandler extends JiraEventHandler { +public class JiraIssueDeleteEventHandler extends JiraIssueAbstractEventHandler { private final String key; public JiraIssueDeleteEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, Long id, @@ -70,7 +70,7 @@ private void handleDeletedMovedIssue(String type) { context.destinationJiraClient().update(destinationKey, updated); - prepareTransition() + prepareTransition(issue) .ifPresent(transition -> context.destinationJiraClient().transition(destinationKey, transition)); context.destinationJiraClient().archive(destinationKey); @@ -80,11 +80,12 @@ private void handleDeletedMovedIssue(String type) { } } - private Optional prepareTransition() { - Optional deletedTransition = context.projectGroup().statuses().deletedTransition(); - if (deletedTransition.isPresent()) { + private Optional prepareTransition(JiraIssue issue) { + Optional deletedStatus = context.projectGroup().statuses().deletedStatus(); + if (deletedStatus.isPresent()) { + prepareTransition(deletedStatus.get(), issue); JiraTransition transition = new JiraTransition(); - transition.transition = new JiraIssueTransition(deletedTransition.get()); + transition.transition = new JiraIssueTransition(deletedStatus.get()); Optional deletedResolution = context.projectGroup().statuses().deletedResolution(); deletedResolution.ifPresent( diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraStaticFieldMappingCache.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraStaticFieldMappingCache.java index 0fca24d..fae0837 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraStaticFieldMappingCache.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraStaticFieldMappingCache.java @@ -23,9 +23,9 @@ public static String issueType(String projectGroup, String sourceId, return issueType.computeIfAbsent(projectGroup, onMissing).getOrDefault(sourceId, defaultValue); } - public static String status(String projectGroup, String sourceId, Function> onMissing, - String defaultValue) { - return status.computeIfAbsent(projectGroup, onMissing).getOrDefault(sourceId, defaultValue); + public static String status(String projectGroup, String transitionKey, Function onMissing) { + return status.computeIfAbsent(projectGroup, pg -> new ConcurrentHashMap<>()).computeIfAbsent(transitionKey, + onMissing); } public static String linkType(String projectGroup, String sourceId, Function> onMissing, diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraIssueTransition.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraIssueTransition.java index f22444e..5546450 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraIssueTransition.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraIssueTransition.java @@ -1,9 +1,7 @@ package org.hibernate.infra.replicate.jira.service.jira.model.rest; -import org.hibernate.infra.replicate.jira.service.jira.model.JiraBaseObject; - -public class JiraIssueTransition extends JiraBaseObject { - public String id; +public class JiraIssueTransition extends JiraSimpleObject { + public JiraSimpleObject to; public JiraIssueTransition() { } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraTransitions.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraTransitions.java new file mode 100644 index 0000000..ce69522 --- /dev/null +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraTransitions.java @@ -0,0 +1,9 @@ +package org.hibernate.infra.replicate.jira.service.jira.model.rest; + +import java.util.List; + +import org.hibernate.infra.replicate.jira.service.jira.model.JiraBaseObject; + +public class JiraTransitions extends JiraBaseObject { + public List transitions; +} 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 da52e9a..8792aff 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 @@ -19,10 +19,12 @@ import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssueLink; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssueLinkTypes; 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.JiraRemoteLink; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraSimpleObject; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTransition; +import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraTransitions; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraUser; import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraVersion; @@ -190,6 +192,18 @@ public void transition(String issueKey, JiraTransition transition) { // do nothing } + @Override + public JiraTransitions availableTransitions(String issueKey) { + JiraTransitions transitions = new JiraTransitions(); + JiraIssueTransition transition = new JiraIssueTransition(); + transition.name = "To To Do"; + transition.id = "100"; + transition.to = new JiraSimpleObject("1234"); + transition.to.name = "To Do"; + transitions.transitions = List.of(transition); + return transitions; + } + @Override public void archive(String issueKey) { // do nothing diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index ab6c16d..ceb359f 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -14,6 +14,9 @@ jira.project-group."hibernate".destination.api-uri=http://localhost:8081/api/jir jira.project-group."hibernate".destination.api-user.email=user-name jira.project-group."hibernate".destination.api-user.token=user-token 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".projects.JIRATEST1.project-id=10323 jira.project-group."hibernate".projects.JIRATEST1.project-key=JIRATEST2