Skip to content

Commit

Permalink
Use "jira app links" for remote links if possible
Browse files Browse the repository at this point in the history
- make upstream link look as if it's a jira link
- adjust how issue links are handled
  • Loading branch information
marko-bekhta committed Oct 30, 2024
1 parent a79fb87 commit ca4dc10
Show file tree
Hide file tree
Showing 9 changed files with 143 additions and 32 deletions.
13 changes: 13 additions & 0 deletions src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,19 @@ interface IssueLinkTypeValueMapping extends ValueMapping {
* adding an extra link for it.
*/
String parentLinkType();

/**
* @return the name to be set as a part of the remote link request in the
* `application` object i.e. the name of "remote jira" as configured on
* your server.
*/
Optional<String> applicationNameForRemoteLinkType();

/**
* @return the appId to be used to create a globalId for a remote link, e.g.:
* {@code "globalId": "appId=5e7d6222-8225-3bcd-be58-5fe3980b0fae&issueId=65806"}
*/
Optional<String> applicationIdForRemoteLinkType();
}

interface IssueTypeValueMapping extends ValueMapping {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.hibernate.infra.replicate.jira.service.jira;

import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
Expand Down Expand Up @@ -35,8 +36,11 @@ public final class HandlerProjectContext implements AutoCloseable {
private final String projectKeyWithDash;
private final JiraUser notMappedAssignee;

private final Map<String, HandlerProjectContext> allProjectsContextMap;

public HandlerProjectContext(String projectName, String projectGroupName, JiraRestClient sourceJiraClient,
JiraRestClient destinationJiraClient, HandlerProjectGroupContext projectGroupContext) {
JiraRestClient destinationJiraClient, HandlerProjectGroupContext projectGroupContext,
Map<String, HandlerProjectContext> allProjectsContextMap) {
this.projectName = projectName;
this.projectGroupName = projectGroupName;
this.sourceJiraClient = sourceJiraClient;
Expand All @@ -49,6 +53,8 @@ public HandlerProjectContext(String projectName, String projectGroupName, JiraRe

this.notMappedAssignee = projectGroup().users().notMappedAssignee()
.map(v -> new JiraUser(projectGroup().users().mappedPropertyName(), v)).orElse(null);

this.allProjectsContextMap = allProjectsContextMap;
}

public JiraConfig.JiraProject project() {
Expand Down Expand Up @@ -203,4 +209,12 @@ public int pendingEventsInCurrentContext() {
public void submitTask(Runnable runnable) {
projectGroupContext.submitTask(runnable);
}

public Optional<HandlerProjectContext> contextForProjectInSameGroup(String project) {
if (!projectGroup().projects().containsKey(project)) {
// different project group, don't bother
return Optional.empty();
}
return Optional.ofNullable(allProjectsContextMap.get(project));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,14 @@ public class JiraService {

@Inject
public JiraService(JiraConfig jiraConfig, ReportingConfig reportingConfig, Scheduler scheduler) {

Map<String, HandlerProjectContext> 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.put(project.getKey(), new HandlerProjectContext(project.getKey(), entry.getKey(), source,
destination, groupContext, contextMap));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,11 @@ protected String toDestinationKey(String key) {
return key;
}

protected String toProjectFromKey(String key) {
int index = key.lastIndexOf('-');
return index > 0 ? key.substring(0, index) : null;
}

public abstract String toString();

protected record UserData(String name, URI uri) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,23 @@ protected JiraRemoteLink remoteSelfLink(JiraIssue sourceIssue) {

JiraRemoteLink link = new JiraRemoteLink();
// >> Setting this field enables the remote issue link details to be updated or
// deleted using remote system
// >> and item details as the record identifier, rather than using the record's
// Jira ID.
// >> deleted using remote system and item details as the record identifier,
// >> rather than using the record's Jira ID.
//
// Hence, we set this global id as a link to the issue, this way it should be
// unique enough and easy to create:
link.globalId = jiraLink.toString();
// And if the appid/names are available then we can make it also look as if it
// is not a remote link:

Optional<String> appId = context.projectGroup().issueLinkTypes().applicationIdForRemoteLinkType();
link.globalId = appId.map(s -> "appId=%s&issueId=%s".formatted(s, sourceIssue.id))
.orElseGet(jiraLink::toString);

link.relationship = "Upstream issue";
link.object.title = sourceIssue.key;
link.object.url = jiraLink;
link.object.summary = "Link to an upstream JIRA issue, from which this one was cloned.";

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package org.hibernate.infra.replicate.jira.service.jira.handler;

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.client.JiraRestException;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssue;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraIssueLink;
import org.hibernate.infra.replicate.jira.service.jira.model.rest.JiraRemoteLink;
import org.hibernate.infra.replicate.jira.service.reporting.ReportingConfig;

import jakarta.ws.rs.core.UriBuilder;

public class JiraIssueLinkUpsertEventHandler extends JiraEventHandler {

public JiraIssueLinkUpsertEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, Long id) {
Expand All @@ -26,25 +32,61 @@ protected void doRun() {
// make sure that both sides of the link exist:
String outwardIssue = toDestinationKey(sourceLink.outwardIssue.key);
String inwardIssue = toDestinationKey(sourceLink.inwardIssue.key);
context.createNextPlaceholderBatch(outwardIssue);
context.createNextPlaceholderBatch(inwardIssue);
JiraIssue issue = context.destinationJiraClient().getIssue(inwardIssue);

if (issue.fields.issuelinks != null) {
// do we already have this issue link or not ?
for (JiraIssueLink issuelink : issue.fields.issuelinks) {
if ((outwardIssue.equals(issuelink.outwardIssue.key) || inwardIssue.equals(issuelink.inwardIssue.key))
&& issuelink.type.name.equals(sourceLink.type.name)) {
return;

Optional<HandlerProjectContext> outwardContext = context
.contextForProjectInSameGroup(toProjectFromKey(outwardIssue));

Optional<HandlerProjectContext> inwardContext = context
.contextForProjectInSameGroup(toProjectFromKey(inwardIssue));
if (inwardContext.isPresent() && outwardContext.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);
JiraIssue issue = context.destinationJiraClient().getIssue(inwardIssue);

if (issue.fields.issuelinks != null) {
// do we already have this issue link or not ?
for (JiraIssueLink issuelink : issue.fields.issuelinks) {
if ((outwardIssue.equals(issuelink.outwardIssue.key)
|| inwardIssue.equals(issuelink.inwardIssue.key))
&& issuelink.type.name.equals(sourceLink.type.name)) {
return;
}
}
}

JiraIssueLink toCreate = new JiraIssueLink();
toCreate.type.id = linkType(sourceLink.type.id).orElse(null);
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 {
failureCollector.warning("Couldn't find a suitable way to process the issue link for %s".formatted(this));
}
}

private void createAsRemoteLink(JiraIssueLink sourceLink, String linkedIssueKey, String linkedIssueId,
String currentIssue) {
URI jiraLink = UriBuilder.fromUri(sourceLink.self).replacePath("browse").path(linkedIssueKey).build();
JiraRemoteLink link = new JiraRemoteLink();

Optional<String> appId = context.projectGroup().issueLinkTypes().applicationIdForRemoteLinkType();
link.globalId = appId.map(s -> "appId=%s&issueId=%s".formatted(s, linkedIssueId))
.orElseGet(() -> sourceLink.self.toString());
link.relationship = sourceLink.type.name;
link.object.title = linkedIssueKey;
link.object.url = jiraLink;

JiraIssueLink toCreate = new JiraIssueLink();
toCreate.type.id = linkType(sourceLink.type.id).orElse(null);
toCreate.inwardIssue.key = inwardIssue;
toCreate.outwardIssue.key = outwardIssue;
context.destinationJiraClient().upsertIssueLink(toCreate);
Optional<String> applicationName = context.projectGroup().issueLinkTypes().applicationNameForRemoteLinkType();
link.application = applicationName.map(JiraRemoteLink.Application::new).orElse(null);
context.destinationJiraClient().upsertRemoteLink(currentIssue, link);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class JiraRemoteLink extends JiraBaseObject {
public URI self;
public String relationship;
public LinkObject object = new LinkObject();
public Application application;

@Override
public String toString() {
Expand All @@ -26,4 +27,21 @@ public String toString() {
return "LinkObject{" + "summary='" + summary + '\'' + ", title='" + title + '\'' + ", url=" + url + '}';
}
}

public static class Application extends JiraBaseObject {
public String name;
public String type;

public Application() {
}

public Application(String name) {
this(name, "com.atlassian.jira");
}

public Application(String name, String type) {
this.name = name;
this.type = type;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
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;
Expand Down Expand Up @@ -45,8 +48,14 @@ class IssueTest {

@BeforeEach
void setUp() {
context = new HandlerProjectContext("JIRATEST1", PROJECT_GROUP_NAME, source, destination,
new HandlerProjectGroupContext(jiraConfig.projectGroup().get(PROJECT_GROUP_NAME)));
Map<String, HandlerProjectContext> 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));
}

@AfterEach
Expand Down
16 changes: 11 additions & 5 deletions src/test/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ 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
%dev,test.jira.project-group."hibernate".projects.JIRATEST1.security.secret=not-a-secret
%dev,test.jira.project-group."hibernate".projects.JIRATEST1.project-id=10323
%dev,test.jira.project-group."hibernate".projects.JIRATEST1.project-key=JIRATEST2
%dev,test.jira.project-group."hibernate".projects.JIRATEST1.original-project-key=JIRATEST1
%dev,test.jira.project-group."hibernate".projects.JIRATEST1.security.enabled=true
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
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

0 comments on commit ca4dc10

Please sign in to comment.