Skip to content

Commit ca4dc10

Browse files
committed
Use "jira app links" for remote links if possible
- make upstream link look as if it's a jira link - adjust how issue links are handled
1 parent a79fb87 commit ca4dc10

File tree

9 files changed

+143
-32
lines changed

9 files changed

+143
-32
lines changed

src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,19 @@ interface IssueLinkTypeValueMapping extends ValueMapping {
232232
* adding an extra link for it.
233233
*/
234234
String parentLinkType();
235+
236+
/**
237+
* @return the name to be set as a part of the remote link request in the
238+
* `application` object i.e. the name of "remote jira" as configured on
239+
* your server.
240+
*/
241+
Optional<String> applicationNameForRemoteLinkType();
242+
243+
/**
244+
* @return the appId to be used to create a globalId for a remote link, e.g.:
245+
* {@code "globalId": "appId=5e7d6222-8225-3bcd-be58-5fe3980b0fae&issueId=65806"}
246+
*/
247+
Optional<String> applicationIdForRemoteLinkType();
235248
}
236249

237250
interface IssueTypeValueMapping extends ValueMapping {

src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.hibernate.infra.replicate.jira.service.jira;
22

3+
import java.util.Map;
34
import java.util.Optional;
45
import java.util.concurrent.atomic.AtomicLong;
56
import java.util.concurrent.locks.ReentrantLock;
@@ -35,8 +36,11 @@ public final class HandlerProjectContext implements AutoCloseable {
3536
private final String projectKeyWithDash;
3637
private final JiraUser notMappedAssignee;
3738

39+
private final Map<String, HandlerProjectContext> allProjectsContextMap;
40+
3841
public HandlerProjectContext(String projectName, String projectGroupName, JiraRestClient sourceJiraClient,
39-
JiraRestClient destinationJiraClient, HandlerProjectGroupContext projectGroupContext) {
42+
JiraRestClient destinationJiraClient, HandlerProjectGroupContext projectGroupContext,
43+
Map<String, HandlerProjectContext> allProjectsContextMap) {
4044
this.projectName = projectName;
4145
this.projectGroupName = projectGroupName;
4246
this.sourceJiraClient = sourceJiraClient;
@@ -49,6 +53,8 @@ public HandlerProjectContext(String projectName, String projectGroupName, JiraRe
4953

5054
this.notMappedAssignee = projectGroup().users().notMappedAssignee()
5155
.map(v -> new JiraUser(projectGroup().users().mappedPropertyName(), v)).orElse(null);
56+
57+
this.allProjectsContextMap = allProjectsContextMap;
5258
}
5359

5460
public JiraConfig.JiraProject project() {
@@ -203,4 +209,12 @@ public int pendingEventsInCurrentContext() {
203209
public void submitTask(Runnable runnable) {
204210
projectGroupContext.submitTask(runnable);
205211
}
212+
213+
public Optional<HandlerProjectContext> contextForProjectInSameGroup(String project) {
214+
if (!projectGroup().projects().containsKey(project)) {
215+
// different project group, don't bother
216+
return Optional.empty();
217+
}
218+
return Optional.ofNullable(allProjectsContextMap.get(project));
219+
}
206220
}

src/main/java/org/hibernate/infra/replicate/jira/service/jira/JiraService.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,14 @@ public class JiraService {
5353

5454
@Inject
5555
public JiraService(JiraConfig jiraConfig, ReportingConfig reportingConfig, Scheduler scheduler) {
56-
5756
Map<String, HandlerProjectContext> contextMap = new HashMap<>();
5857
for (var entry : jiraConfig.projectGroup().entrySet()) {
5958
JiraRestClient source = JiraRestClientBuilder.of(entry.getValue().source());
6059
JiraRestClient destination = JiraRestClientBuilder.of(entry.getValue().destination());
6160
HandlerProjectGroupContext groupContext = new HandlerProjectGroupContext(entry.getValue());
6261
for (var project : entry.getValue().projects().entrySet()) {
63-
contextMap.put(project.getKey(),
64-
new HandlerProjectContext(project.getKey(), entry.getKey(), source, destination, groupContext));
62+
contextMap.put(project.getKey(), new HandlerProjectContext(project.getKey(), entry.getKey(), source,
63+
destination, groupContext, contextMap));
6564
}
6665
}
6766

src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraEventHandler.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ protected String toDestinationKey(String key) {
221221
return key;
222222
}
223223

224+
protected String toProjectFromKey(String key) {
225+
int index = key.lastIndexOf('-');
226+
return index > 0 ? key.substring(0, index) : null;
227+
}
228+
224229
public abstract String toString();
225230

226231
protected record UserData(String name, URI uri) {

src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,23 @@ protected JiraRemoteLink remoteSelfLink(JiraIssue sourceIssue) {
6363

6464
JiraRemoteLink link = new JiraRemoteLink();
6565
// >> Setting this field enables the remote issue link details to be updated or
66-
// deleted using remote system
67-
// >> and item details as the record identifier, rather than using the record's
68-
// Jira ID.
66+
// >> deleted using remote system and item details as the record identifier,
67+
// >> rather than using the record's Jira ID.
6968
//
70-
// Hence, we set this global id as a link to the issue, this way it should be
71-
// unique enough and easy to create:
72-
link.globalId = jiraLink.toString();
69+
// And if the appid/names are available then we can make it also look as if it
70+
// is not a remote link:
71+
72+
Optional<String> appId = context.projectGroup().issueLinkTypes().applicationIdForRemoteLinkType();
73+
link.globalId = appId.map(s -> "appId=%s&issueId=%s".formatted(s, sourceIssue.id))
74+
.orElseGet(jiraLink::toString);
75+
7376
link.relationship = "Upstream issue";
7477
link.object.title = sourceIssue.key;
7578
link.object.url = jiraLink;
7679
link.object.summary = "Link to an upstream JIRA issue, from which this one was cloned.";
7780

81+
Optional<String> applicationName = context.projectGroup().issueLinkTypes().applicationNameForRemoteLinkType();
82+
link.application = applicationName.map(JiraRemoteLink.Application::new).orElse(null);
7883
return link;
7984
}
8085

src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueLinkUpsertEventHandler.java

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
package org.hibernate.infra.replicate.jira.service.jira.handler;
22

3+
import java.net.URI;
4+
import java.util.Optional;
5+
36
import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext;
47
import org.hibernate.infra.replicate.jira.service.jira.client.JiraRestException;
58
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue;
69
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssueLink;
10+
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraRemoteLink;
711
import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig;
812

13+
import jakarta.ws.rs.core.UriBuilder;
14+
915
public class JiraIssueLinkUpsertEventHandler extends JiraEventHandler {
1016

1117
public JiraIssueLinkUpsertEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, Long id) {
@@ -26,25 +32,61 @@ protected void doRun() {
2632
// make sure that both sides of the link exist:
2733
String outwardIssue = toDestinationKey(sourceLink.outwardIssue.key);
2834
String inwardIssue = toDestinationKey(sourceLink.inwardIssue.key);
29-
context.createNextPlaceholderBatch(outwardIssue);
30-
context.createNextPlaceholderBatch(inwardIssue);
31-
JiraIssue issue = context.destinationJiraClient().getIssue(inwardIssue);
32-
33-
if (issue.fields.issuelinks != null) {
34-
// do we already have this issue link or not ?
35-
for (JiraIssueLink issuelink : issue.fields.issuelinks) {
36-
if ((outwardIssue.equals(issuelink.outwardIssue.key) || inwardIssue.equals(issuelink.inwardIssue.key))
37-
&& issuelink.type.name.equals(sourceLink.type.name)) {
38-
return;
35+
36+
Optional<HandlerProjectContext> outwardContext = context
37+
.contextForProjectInSameGroup(toProjectFromKey(outwardIssue));
38+
39+
Optional<HandlerProjectContext> inwardContext = context
40+
.contextForProjectInSameGroup(toProjectFromKey(inwardIssue));
41+
if (inwardContext.isPresent() && outwardContext.isPresent()) {
42+
// means we want to create a simple issue between two projects in the same
43+
// project group
44+
// so it'll be a regular issue link:
45+
46+
inwardContext.get().createNextPlaceholderBatch(outwardIssue);
47+
outwardContext.get().createNextPlaceholderBatch(inwardIssue);
48+
JiraIssue issue = context.destinationJiraClient().getIssue(inwardIssue);
49+
50+
if (issue.fields.issuelinks != null) {
51+
// do we already have this issue link or not ?
52+
for (JiraIssueLink issuelink : issue.fields.issuelinks) {
53+
if ((outwardIssue.equals(issuelink.outwardIssue.key)
54+
|| inwardIssue.equals(issuelink.inwardIssue.key))
55+
&& issuelink.type.name.equals(sourceLink.type.name)) {
56+
return;
57+
}
3958
}
4059
}
60+
61+
JiraIssueLink toCreate = new JiraIssueLink();
62+
toCreate.type.id = linkType(sourceLink.type.id).orElse(null);
63+
toCreate.inwardIssue.key = inwardIssue;
64+
toCreate.outwardIssue.key = outwardIssue;
65+
context.destinationJiraClient().upsertIssueLink(toCreate);
66+
} else if (outwardContext.isPresent()) {
67+
createAsRemoteLink(sourceLink, inwardIssue, sourceLink.inwardIssue.id, outwardIssue);
68+
} else if (inwardContext.isPresent()) {
69+
createAsRemoteLink(sourceLink, outwardIssue, sourceLink.outwardIssue.id, inwardIssue);
70+
} else {
71+
failureCollector.warning("Couldn't find a suitable way to process the issue link for %s".formatted(this));
4172
}
73+
}
74+
75+
private void createAsRemoteLink(JiraIssueLink sourceLink, String linkedIssueKey, String linkedIssueId,
76+
String currentIssue) {
77+
URI jiraLink = UriBuilder.fromUri(sourceLink.self).replacePath("browse").path(linkedIssueKey).build();
78+
JiraRemoteLink link = new JiraRemoteLink();
79+
80+
Optional<String> appId = context.projectGroup().issueLinkTypes().applicationIdForRemoteLinkType();
81+
link.globalId = appId.map(s -> "appId=%s&issueId=%s".formatted(s, linkedIssueId))
82+
.orElseGet(() -> sourceLink.self.toString());
83+
link.relationship = sourceLink.type.name;
84+
link.object.title = linkedIssueKey;
85+
link.object.url = jiraLink;
4286

43-
JiraIssueLink toCreate = new JiraIssueLink();
44-
toCreate.type.id = linkType(sourceLink.type.id).orElse(null);
45-
toCreate.inwardIssue.key = inwardIssue;
46-
toCreate.outwardIssue.key = outwardIssue;
47-
context.destinationJiraClient().upsertIssueLink(toCreate);
87+
Optional<String> applicationName = context.projectGroup().issueLinkTypes().applicationNameForRemoteLinkType();
88+
link.application = applicationName.map(JiraRemoteLink.Application::new).orElse(null);
89+
context.destinationJiraClient().upsertRemoteLink(currentIssue, link);
4890
}
4991

5092
@Override

src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraRemoteLink.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public class JiraRemoteLink extends JiraBaseObject {
99
public URI self;
1010
public String relationship;
1111
public LinkObject object = new LinkObject();
12+
public Application application;
1213

1314
@Override
1415
public String toString() {
@@ -26,4 +27,21 @@ public String toString() {
2627
return "LinkObject{" + "summary='" + summary + '\'' + ", title='" + title + '\'' + ", url=" + url + '}';
2728
}
2829
}
30+
31+
public static class Application extends JiraBaseObject {
32+
public String name;
33+
public String type;
34+
35+
public Application() {
36+
}
37+
38+
public Application(String name) {
39+
this(name, "com.atlassian.jira");
40+
}
41+
42+
public Application(String name, String type) {
43+
this.name = name;
44+
this.type = type;
45+
}
46+
}
2947
}

src/test/java/org/hibernate/infra/replicate/jira/handler/IssueTest.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import static org.mockito.ArgumentMatchers.any;
55
import static org.mockito.ArgumentMatchers.eq;
66

7+
import java.util.HashMap;
8+
import java.util.Map;
9+
710
import org.hibernate.infra.replicate.jira.JiraConfig;
811
import org.hibernate.infra.replicate.jira.mock.SampleJiraRestClient;
912
import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext;
@@ -45,8 +48,14 @@ class IssueTest {
4548

4649
@BeforeEach
4750
void setUp() {
48-
context = new HandlerProjectContext("JIRATEST1", PROJECT_GROUP_NAME, source, destination,
49-
new HandlerProjectGroupContext(jiraConfig.projectGroup().get(PROJECT_GROUP_NAME)));
51+
Map<String, HandlerProjectContext> contextMap = new HashMap<>();
52+
HandlerProjectGroupContext projectGroupContext = new HandlerProjectGroupContext(
53+
jiraConfig.projectGroup().get(PROJECT_GROUP_NAME));
54+
context = new HandlerProjectContext("JIRATEST1", PROJECT_GROUP_NAME, source, destination, projectGroupContext,
55+
contextMap);
56+
contextMap.put("JIRATEST1", context);
57+
contextMap.put("JIRATEST2", new HandlerProjectContext("JIRATEST2", PROJECT_GROUP_NAME, source, destination,
58+
projectGroupContext, contextMap));
5059
}
5160

5261
@AfterEach

src/test/resources/application.properties

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,14 @@ jira.project-group."hibernate".destination.api-uri=http://localhost:8081/api/jir
1414
jira.project-group."hibernate".destination.api-user.email=user-name
1515
jira.project-group."hibernate".destination.api-user.token=user-token
1616
jira.project-group."hibernate".issue-link-types.default-value=10050
17-
%dev,test.jira.project-group."hibernate".projects.JIRATEST1.security.secret=not-a-secret
18-
%dev,test.jira.project-group."hibernate".projects.JIRATEST1.project-id=10323
19-
%dev,test.jira.project-group."hibernate".projects.JIRATEST1.project-key=JIRATEST2
20-
%dev,test.jira.project-group."hibernate".projects.JIRATEST1.original-project-key=JIRATEST1
21-
%dev,test.jira.project-group."hibernate".projects.JIRATEST1.security.enabled=true
17+
jira.project-group."hibernate".projects.JIRATEST1.security.secret=not-a-secret
18+
jira.project-group."hibernate".projects.JIRATEST1.project-id=10323
19+
jira.project-group."hibernate".projects.JIRATEST1.project-key=JIRATEST2
20+
jira.project-group."hibernate".projects.JIRATEST1.original-project-key=JIRATEST1
21+
jira.project-group."hibernate".projects.JIRATEST1.security.enabled=true
22+
23+
jira.project-group."hibernate".projects.JIRATEST2.security.secret=not-a-secret
24+
jira.project-group."hibernate".projects.JIRATEST2.project-id=10324
25+
jira.project-group."hibernate".projects.JIRATEST2.project-key=JIRATEST2
26+
jira.project-group."hibernate".projects.JIRATEST2.original-project-key=JIRATEST2
27+
jira.project-group."hibernate".projects.JIRATEST2.security.enabled=true

0 commit comments

Comments
 (0)