diff --git a/api-channel-issue-tracker/build.gradle b/api-channel-issue-tracker/build.gradle index 1ec1796d0e..9ecde29331 100644 --- a/api-channel-issue-tracker/build.gradle +++ b/api-channel-issue-tracker/build.gradle @@ -2,6 +2,7 @@ ext.moduleName = 'com.blackduck.integration.alert.api-channel-issue-tracker' dependencies { api project(':api-channel') + api 'com.blackduck.integration:int-jira-common' implementation platform(project(':alert-platform')) implementation project(':alert-common') diff --git a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerChannel.java b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerChannel.java index 132c4c7acc..e7b8b86981 100644 --- a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerChannel.java +++ b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerChannel.java @@ -42,7 +42,7 @@ public MessageResult distributeMessages( ) throws AlertException { - IssueTrackerProcessor processor = processorFactory.createProcessor(distributionDetails, jobExecutionId, notificationIds); + IssueTrackerMessageProcessor processor = processorFactory.createProcessor(distributionDetails, jobExecutionId, notificationIds); IssueTrackerResponse issueTrackerResponse = processor.processMessages(messages, jobName); return new MessageResult(issueTrackerResponse.getStatusMessage()); diff --git a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerMessageProcessor.java b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerMessageProcessor.java new file mode 100644 index 0000000000..031b4961df --- /dev/null +++ b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerMessageProcessor.java @@ -0,0 +1,19 @@ +/* + * blackduck-alert + * + * Copyright (c) 2025 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.api.channel.issue.tracker; + +import java.io.Serializable; + +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerResponse; +import com.blackduck.integration.alert.api.common.model.exception.AlertException; +import com.blackduck.integration.alert.api.processor.extract.model.ProviderMessageHolder; + +public interface IssueTrackerMessageProcessor { + + IssueTrackerResponse processMessages(ProviderMessageHolder messages, String jobName) throws AlertException; +} diff --git a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerProcessor.java b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerProcessor.java index c8175ade9f..9219330f38 100644 --- a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerProcessor.java +++ b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerProcessor.java @@ -11,22 +11,23 @@ import java.util.LinkedList; import java.util.List; -import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerAsyncMessageSender; import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerModelHolder; import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerResponse; +import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerAsyncMessageSender; import com.blackduck.integration.alert.api.common.model.exception.AlertException; import com.blackduck.integration.alert.api.processor.extract.model.ProviderMessageHolder; import com.blackduck.integration.alert.api.processor.extract.model.project.ProjectMessage; -public class IssueTrackerProcessor { +public class IssueTrackerProcessor implements IssueTrackerMessageProcessor { private final IssueTrackerModelExtractor modelExtractor; - private final IssueTrackerAsyncMessageSender messageSender; + private final IssueTrackerAsyncMessageSender> messageSender; - public IssueTrackerProcessor(IssueTrackerModelExtractor modelExtractor, IssueTrackerAsyncMessageSender messageSender) { + public IssueTrackerProcessor(IssueTrackerModelExtractor modelExtractor, IssueTrackerAsyncMessageSender> messageSender) { this.modelExtractor = modelExtractor; this.messageSender = messageSender; } + @Override public final IssueTrackerResponse processMessages(ProviderMessageHolder messages, String jobName) throws AlertException { List> issueTrackerModels = new LinkedList<>(); IssueTrackerModelHolder simpleMessageHolder = modelExtractor.extractSimpleMessageIssueModels(messages.getSimpleMessages(), jobName); diff --git a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerProcessorFactory.java b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerProcessorFactory.java index bf896f00be..5bc87982b9 100644 --- a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerProcessorFactory.java +++ b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerProcessorFactory.java @@ -15,6 +15,6 @@ import com.blackduck.integration.alert.common.persistence.model.job.details.DistributionJobDetailsModel; public interface IssueTrackerProcessorFactory { - IssueTrackerProcessor createProcessor(D distributionDetails, UUID jobExecutionId, Set notificationIds) throws AlertException; + IssueTrackerMessageProcessor createProcessor(D distributionDetails, UUID jobExecutionId, Set notificationIds) throws AlertException; } diff --git a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/action/IssueTrackerTestAction.java b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/action/IssueTrackerTestAction.java index fb0269da0f..d0016c1eaa 100644 --- a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/action/IssueTrackerTestAction.java +++ b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/action/IssueTrackerTestAction.java @@ -15,17 +15,19 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueBomComponentDetails; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueCreationModel; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerIssueResponseModel; import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerModelHolder; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.ProjectIssueModel; import com.blackduck.integration.alert.api.channel.issue.tracker.search.ExistingIssueDetails; import com.blackduck.integration.alert.api.channel.issue.tracker.search.enumeration.IssueCategory; import com.blackduck.integration.alert.api.channel.issue.tracker.search.enumeration.IssueStatus; import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerMessageSender; import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerMessageSenderFactory; -import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueBomComponentDetails; -import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueCreationModel; -import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerIssueResponseModel; -import com.blackduck.integration.alert.api.channel.issue.tracker.model.ProjectIssueModel; import com.blackduck.integration.alert.api.common.model.exception.AlertException; +import com.blackduck.integration.alert.api.descriptor.model.IssueTrackerChannelKey; +import com.blackduck.integration.alert.api.processor.extract.model.ProviderDetails; import com.blackduck.integration.alert.common.channel.DistributionChannelTestAction; import com.blackduck.integration.alert.common.channel.issuetracker.enumeration.IssueOperation; import com.blackduck.integration.alert.common.exception.AlertFieldException; @@ -33,15 +35,13 @@ import com.blackduck.integration.alert.common.message.model.MessageResult; import com.blackduck.integration.alert.common.persistence.model.job.DistributionJobModel; import com.blackduck.integration.alert.common.persistence.model.job.details.DistributionJobDetailsModel; -import com.blackduck.integration.alert.api.descriptor.model.IssueTrackerChannelKey; -import com.blackduck.integration.alert.api.processor.extract.model.ProviderDetails; public abstract class IssueTrackerTestAction extends DistributionChannelTestAction { private final Logger logger = LoggerFactory.getLogger(getClass()); - private final IssueTrackerMessageSenderFactory messageSenderFactory; + private final IssueTrackerMessageSenderFactory> messageSenderFactory; - protected IssueTrackerTestAction(IssueTrackerChannelKey issueTrackerChannelKey, IssueTrackerMessageSenderFactory messageSenderFactory) { + protected IssueTrackerTestAction(IssueTrackerChannelKey issueTrackerChannelKey, IssueTrackerMessageSenderFactory> messageSenderFactory) { super(issueTrackerChannelKey); this.messageSenderFactory = messageSenderFactory; } diff --git a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/model/IssueCommentModel.java b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/model/IssueCommentModel.java index df57e2f270..9fde2fbf6d 100644 --- a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/model/IssueCommentModel.java +++ b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/model/IssueCommentModel.java @@ -15,6 +15,7 @@ import com.blackduck.integration.alert.api.channel.issue.tracker.search.ExistingIssueDetails; import com.blackduck.integration.alert.api.common.model.AlertSerializableModel; +import com.blackduck.integration.jira.common.cloud.model.AtlassianDocumentFormatModel; public class IssueCommentModel extends AlertSerializableModel { private final ExistingIssueDetails existingIssueDetails; @@ -22,10 +23,29 @@ public class IssueCommentModel extends AlertSerializable @Nullable private final ProjectIssueModel source; + // Only used for Jira Cloud channel. + // the reason these Jira Cloud specific fields are here is because of the layers of abstraction prevent having a model specific for Jira cloud. + @Nullable + private final AtlassianDocumentFormatModel atlassianDocumentFormatCommentModel; + @Nullable + private final List additionalComments; + public IssueCommentModel(ExistingIssueDetails existingIssueDetails, List comments, @Nullable ProjectIssueModel source) { + this(existingIssueDetails, comments, source, null, null); + } + + public IssueCommentModel( + ExistingIssueDetails existingIssueDetails, + List comments, + @Nullable ProjectIssueModel source, + @Nullable AtlassianDocumentFormatModel atlassianDocumentFormatCommentModel, + @Nullable List additionalComments + ) { this.existingIssueDetails = existingIssueDetails; this.comments = comments; this.source = source; + this.atlassianDocumentFormatCommentModel = atlassianDocumentFormatCommentModel; + this.additionalComments = additionalComments; } public ExistingIssueDetails getExistingIssueDetails() { @@ -40,4 +60,12 @@ public Optional getSource() { return Optional.ofNullable(source); } + public Optional getAtlassianDocumentFormatCommentModel() { + return Optional.ofNullable(atlassianDocumentFormatCommentModel); + } + + public Optional> getAdditionalComments() { + return Optional.ofNullable(additionalComments); + } + } diff --git a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/model/IssueCreationModel.java b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/model/IssueCreationModel.java index a96c65b65b..42748a1f8c 100644 --- a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/model/IssueCreationModel.java +++ b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/model/IssueCreationModel.java @@ -14,6 +14,7 @@ import com.blackduck.integration.alert.api.common.model.AlertSerializableModel; import com.blackduck.integration.alert.common.message.model.LinkableItem; +import com.blackduck.integration.jira.common.cloud.model.AtlassianDocumentFormatModel; public class IssueCreationModel extends AlertSerializableModel { private static final long serialVersionUID = -3568919050386494416L; @@ -24,22 +25,58 @@ public class IssueCreationModel extends AlertSerializableModel { private final LinkableItem provider; private final ProjectIssueModel source; + // Only used for Jira Cloud channel. + // the reason these Jira Cloud specific fields are here is because of the layers of abstraction prevent having a model specific for Jira cloud. + private final AtlassianDocumentFormatModel atlassianDocumentFormatDescriptionModel; + private final List atlassianDocumentFormatCommentModel; public static IssueCreationModel simple(String title, String description, List postCreateComments, LinkableItem provider) { - return new IssueCreationModel(title, description, postCreateComments, provider, null, null); + return new IssueCreationModel(title, description, postCreateComments, provider, null, null, null, null); + } + + public static IssueCreationModel simple( + String title, + LinkableItem provider, + AtlassianDocumentFormatModel atlassianDocumentFormatDescriptionModel, + List atlassianDocumentFormatCommentModel + ) { + return new IssueCreationModel(title, "", List.of(), provider, null, null, atlassianDocumentFormatDescriptionModel, atlassianDocumentFormatCommentModel); } public static IssueCreationModel project(String title, String description, List postCreateComments, ProjectIssueModel source, @Nullable String queryString) { - return new IssueCreationModel(title, description, postCreateComments, source.getProvider(), source, queryString); + return new IssueCreationModel(title, description, postCreateComments, source.getProvider(), source, queryString, null, null); + } + + public static IssueCreationModel project( + String title, + String description, + List postCreateComments, + ProjectIssueModel source, + AtlassianDocumentFormatModel atlassianDocumentFormatDescriptionModel, + List atlassianDocumentFormatCommentModel, + @Nullable String queryString + ) { + return new IssueCreationModel( + title, + description, + postCreateComments, + source.getProvider(), + source, + queryString, + atlassianDocumentFormatDescriptionModel, + atlassianDocumentFormatCommentModel + ); } - private IssueCreationModel( + protected IssueCreationModel( String title, String description, List postCreateComments, LinkableItem provider, @Nullable ProjectIssueModel source, - @Nullable String queryString + @Nullable String queryString, + @Nullable AtlassianDocumentFormatModel atlassianDocumentFormatDescriptionModel, + @Nullable List atlassianDocumentFormatCommentModel ) { this.title = title; this.description = description; @@ -47,6 +84,8 @@ private IssueCreationModel( this.provider = provider; this.source = source; this.queryString = queryString; + this.atlassianDocumentFormatDescriptionModel = atlassianDocumentFormatDescriptionModel; + this.atlassianDocumentFormatCommentModel = atlassianDocumentFormatCommentModel; } public Optional getQueryString() { @@ -73,4 +112,11 @@ public Optional getSource() { return Optional.ofNullable(source); } + public Optional getAtlassianDocumentFormatDescriptionModel() { + return Optional.ofNullable(atlassianDocumentFormatDescriptionModel); + } + + public Optional> getAtlassianDocumentFormatCommentModel() { + return Optional.ofNullable(atlassianDocumentFormatCommentModel); + } } diff --git a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/model/IssueTrackerEventModel.java b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/model/IssueTrackerEventModel.java new file mode 100644 index 0000000000..c11f93cdc5 --- /dev/null +++ b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/model/IssueTrackerEventModel.java @@ -0,0 +1,45 @@ +/* + * blackduck-alert + * + * Copyright (c) 2025 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.api.channel.issue.tracker.model; + +import java.util.List; + +import com.blackduck.integration.alert.api.event.AlertEvent; + +public class IssueTrackerEventModel { + private final List creationEvents; + private final List commentEvents; + private final List transitionEvents; + + public IssueTrackerEventModel(List creationEvents, List commentEvents, List transitionEvents) { + this.creationEvents = creationEvents; + this.commentEvents = commentEvents; + this.transitionEvents = transitionEvents; + } + + public List getIssueCreationEvents() { + if (creationEvents == null) { + return List.of(); + } + return creationEvents; + } + + public List getIssueCommentEvents() { + if (commentEvents == null) { + return List.of(); + } + return commentEvents; + } + + public List getIssueTransitionEvents() { + if (transitionEvents == null) { + return List.of(); + } + return transitionEvents; + } +} diff --git a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/model/IssueTrackerModelHolder.java b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/model/IssueTrackerModelHolder.java index e60121f227..7046d849ba 100644 --- a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/model/IssueTrackerModelHolder.java +++ b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/model/IssueTrackerModelHolder.java @@ -12,7 +12,8 @@ import org.apache.commons.collections4.ListUtils; -public class IssueTrackerModelHolder { +public class IssueTrackerModelHolder implements Serializable { + private final List issueCreationModels; private final List> issueTransitionModels; private final List> issueCommentModels; @@ -24,7 +25,11 @@ public static IssueTrackerModelHolder reduce(IssueTr return new IssueTrackerModelHolder<>(unifiedIssueCreationModels, unifiedIssueTransitionModels, unifiedIssueCommentModels); } - public IssueTrackerModelHolder(List issueCreationModels, List> issueTransitionModels, List> issueCommentModels) { + public IssueTrackerModelHolder( + List issueCreationModels, + List> issueTransitionModels, + List> issueCommentModels + ) { this.issueCreationModels = issueCreationModels; this.issueTransitionModels = issueTransitionModels; this.issueCommentModels = issueCommentModels; diff --git a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/AsyncMessageSender.java b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/AsyncMessageSender.java new file mode 100644 index 0000000000..0ca8888875 --- /dev/null +++ b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/AsyncMessageSender.java @@ -0,0 +1,14 @@ +/* + * blackduck-alert + * + * Copyright (c) 2025 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.api.channel.issue.tracker.send; + +import java.util.List; + +public interface AsyncMessageSender { + void sendAsyncMessages(List issueTrackerMessages); +} diff --git a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/DefaultIssueTrackerEventGenerator.java b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/DefaultIssueTrackerEventGenerator.java new file mode 100644 index 0000000000..677e5f3cbd --- /dev/null +++ b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/DefaultIssueTrackerEventGenerator.java @@ -0,0 +1,47 @@ +/* + * blackduck-alert + * + * Copyright (c) 2025 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.api.channel.issue.tracker.send; + +import java.io.Serializable; +import java.util.List; +import java.util.function.Function; + +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerEventModel; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerModelHolder; +import com.blackduck.integration.alert.api.event.AlertEvent; + +public class DefaultIssueTrackerEventGenerator implements IssueTrackerEventGenerator> { + + private final IssueTrackerCreationEventGenerator issueCreateEventGenerator; + private final IssueTrackerTransitionEventGenerator issueTrackerTransitionEventGenerator; + private final IssueTrackerCommentEventGenerator issueTrackerCommentEventGenerator; + + public DefaultIssueTrackerEventGenerator( + IssueTrackerCreationEventGenerator issueCreateEventGenerator, + IssueTrackerTransitionEventGenerator issueTrackerTransitionEventGenerator, + IssueTrackerCommentEventGenerator issueTrackerCommentEventGenerator + ) { + this.issueCreateEventGenerator = issueCreateEventGenerator; + this.issueTrackerTransitionEventGenerator = issueTrackerTransitionEventGenerator; + this.issueTrackerCommentEventGenerator = issueTrackerCommentEventGenerator; + } + + @Override + public IssueTrackerEventModel generateEvents(IssueTrackerModelHolder model) { + List creationEvents = createMessages(model.getIssueCreationModels(), issueCreateEventGenerator::generateEvent); + List transitionEvents = createMessages(model.getIssueTransitionModels(), issueTrackerTransitionEventGenerator::generateEvent); + List commentEvents = createMessages(model.getIssueCommentModels(), issueTrackerCommentEventGenerator::generateEvent); + return new IssueTrackerEventModel(creationEvents, transitionEvents, commentEvents); + } + + private List createMessages(List messages, Function eventGenerator) { + return messages.stream() + .map(eventGenerator) + .toList(); + } +} diff --git a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/IssueTrackerAsyncMessageSender.java b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/IssueTrackerAsyncMessageSender.java index af083ec648..e0abd357af 100644 --- a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/IssueTrackerAsyncMessageSender.java +++ b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/IssueTrackerAsyncMessageSender.java @@ -7,18 +7,15 @@ */ package com.blackduck.integration.alert.api.channel.issue.tracker.send; -import java.io.Serializable; import java.time.Instant; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.UUID; -import java.util.function.Function; -import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; -import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerModelHolder; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerEventModel; import com.blackduck.integration.alert.api.distribution.execution.ExecutingJobManager; import com.blackduck.integration.alert.api.distribution.execution.JobStage; import com.blackduck.integration.alert.api.distribution.execution.JobStageStartedEvent; @@ -26,38 +23,32 @@ import com.blackduck.integration.alert.api.event.EventManager; import com.blackduck.integration.alert.common.enumeration.AuditEntryStatus; -public class IssueTrackerAsyncMessageSender { - private final IssueTrackerCreationEventGenerator issueCreateEventGenerator; - private final IssueTrackerTransitionEventGenerator issueTrackerTransitionEventGenerator; - private final IssueTrackerCommentEventGenerator issueTrackerCommentEventGenerator; +public class IssueTrackerAsyncMessageSender implements AsyncMessageSender { + private final IssueTrackerEventGenerator issueTrackerEventGenerator; private final EventManager eventManager; private final UUID jobExecutionId; private final Set notificationIds; private final ExecutingJobManager executingJobManager; public IssueTrackerAsyncMessageSender( - IssueTrackerCreationEventGenerator issueCreateEventGenerator, - IssueTrackerTransitionEventGenerator issueTrackerTransitionEventGenerator, - IssueTrackerCommentEventGenerator issueTrackerCommentEventGenerator, + IssueTrackerEventGenerator issueTrackerEventGenerator, EventManager eventManager, UUID jobExecutionId, Set notificationIds, ExecutingJobManager executingJobManager ) { - this.issueCreateEventGenerator = issueCreateEventGenerator; - this.issueTrackerTransitionEventGenerator = issueTrackerTransitionEventGenerator; - this.issueTrackerCommentEventGenerator = issueTrackerCommentEventGenerator; + this.issueTrackerEventGenerator = issueTrackerEventGenerator; this.eventManager = eventManager; this.jobExecutionId = jobExecutionId; this.notificationIds = notificationIds; this.executingJobManager = executingJobManager; } - public final void sendAsyncMessages(List> issueTrackerMessages) { + public final void sendAsyncMessages(List issueTrackerMessages) { List eventList = issueTrackerMessages.stream() .map(this::createAlertEvents) .flatMap(List::stream) - .collect(Collectors.toList()); + .toList(); // the full set of notifications to be sent is here. Each event generated is for a subset of notification ids. // some notifications do not produce events which is why the check for the empty event list also exists. @@ -75,11 +66,12 @@ public final void sendAsyncMessages(List> issueTracke } @NotNull - private List createAlertEvents(IssueTrackerModelHolder issueTrackerMessage) { + private List createAlertEvents(T issueTrackerMessage) { List eventList = new LinkedList<>(); - List creationEvents = createMessages(issueTrackerMessage.getIssueCreationModels(), issueCreateEventGenerator::generateEvent); - List transitionEvents = createMessages(issueTrackerMessage.getIssueTransitionModels(), issueTrackerTransitionEventGenerator::generateEvent); - List commentEvents = createMessages(issueTrackerMessage.getIssueCommentModels(), issueTrackerCommentEventGenerator::generateEvent); + IssueTrackerEventModel eventModel = issueTrackerEventGenerator.generateEvents(issueTrackerMessage); + List creationEvents = eventModel.getIssueCreationEvents(); + List transitionEvents = eventModel.getIssueTransitionEvents(); + List commentEvents = eventModel.getIssueCommentEvents(); addEventsAndStartStage(eventList, creationEvents, JobStage.ISSUE_CREATION); addEventsAndStartStage(eventList, transitionEvents, JobStage.ISSUE_TRANSITION); @@ -88,12 +80,6 @@ private List createAlertEvents(IssueTrackerModelHolder issueTrack return eventList; } - private List createMessages(List messages, Function eventGenerator) { - return messages.stream() - .map(eventGenerator::apply) - .collect(Collectors.toList()); - } - private void addEventsAndStartStage(List allEvents, List events, JobStage jobStage) { if (!events.isEmpty()) { eventManager.sendEvent(new JobStageStartedEvent(jobExecutionId, jobStage, Instant.now().toEpochMilli())); diff --git a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/IssueTrackerEventGenerator.java b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/IssueTrackerEventGenerator.java new file mode 100644 index 0000000000..7c1b94ff49 --- /dev/null +++ b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/IssueTrackerEventGenerator.java @@ -0,0 +1,15 @@ +/* + * blackduck-alert + * + * Copyright (c) 2025 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.api.channel.issue.tracker.send; + +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerEventModel; + +public interface IssueTrackerEventGenerator { + IssueTrackerEventModel generateEvents(M model); + +} diff --git a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/IssueTrackerIssueCreator.java b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/IssueTrackerIssueCreator.java index 5dac3847bf..9aa8c6000b 100644 --- a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/IssueTrackerIssueCreator.java +++ b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/IssueTrackerIssueCreator.java @@ -22,9 +22,9 @@ import com.blackduck.integration.alert.api.channel.issue.tracker.model.ProjectIssueModel; import com.blackduck.integration.alert.api.channel.issue.tracker.search.ExistingIssueDetails; import com.blackduck.integration.alert.api.common.model.exception.AlertException; +import com.blackduck.integration.alert.api.descriptor.model.IssueTrackerChannelKey; import com.blackduck.integration.alert.common.channel.issuetracker.enumeration.IssueOperation; import com.blackduck.integration.alert.common.channel.issuetracker.message.IssueTrackerCallbackInfo; -import com.blackduck.integration.alert.api.descriptor.model.IssueTrackerChannelKey; public abstract class IssueTrackerIssueCreator { private final Logger logger = LoggerFactory.getLogger(getClass()); @@ -79,7 +79,7 @@ public final IssueTrackerIssueResponseModel createIssueTrackerIssue(IssueCrea protected abstract void assignAlertSearchProperties(ExistingIssueDetails createdIssueDetails, ProjectIssueModel alertIssueSource) throws AlertException; - private void addPostCreateComments(ExistingIssueDetails issueDetails, IssueCreationModel creationModel, @Nullable ProjectIssueModel projectSource) throws AlertException { + protected void addPostCreateComments(ExistingIssueDetails issueDetails, IssueCreationModel creationModel, @Nullable ProjectIssueModel projectSource) throws AlertException { LinkedList postCreateComments = new LinkedList<>(creationModel.getPostCreateComments()); postCreateComments.addFirst("This issue was automatically created by Alert."); diff --git a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/IssueTrackerMessageSenderFactory.java b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/IssueTrackerMessageSenderFactory.java index 4ff4ccaa7b..598cb79d6d 100644 --- a/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/IssueTrackerMessageSenderFactory.java +++ b/api-channel-issue-tracker/src/main/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/IssueTrackerMessageSenderFactory.java @@ -16,10 +16,10 @@ import com.blackduck.integration.alert.api.common.model.exception.AlertException; import com.blackduck.integration.alert.common.persistence.model.job.details.DistributionJobDetailsModel; -public interface IssueTrackerMessageSenderFactory { +public interface IssueTrackerMessageSenderFactory { IssueTrackerMessageSender createMessageSender(D distributionDetails, @Nullable UUID globalId) throws AlertException; - IssueTrackerAsyncMessageSender createAsyncMessageSender( + AsyncMessageSender createAsyncMessageSender( D distributionDetails, @Nullable UUID globalId, UUID jobExecutionId, Set notificationIds ) throws AlertException; diff --git a/api-channel-issue-tracker/src/test/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerChannelTest.java b/api-channel-issue-tracker/src/test/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerChannelTest.java index 336ce333b8..301b0d53f0 100644 --- a/api-channel-issue-tracker/src/test/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerChannelTest.java +++ b/api-channel-issue-tracker/src/test/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerChannelTest.java @@ -18,7 +18,9 @@ import org.springframework.core.task.SyncTaskExecutor; import com.blackduck.integration.alert.api.channel.issue.tracker.convert.IssueTrackerMessageFormatter; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerModelHolder; import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerResponse; +import com.blackduck.integration.alert.api.channel.issue.tracker.send.DefaultIssueTrackerEventGenerator; import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerAsyncMessageSender; import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerCommentEventGenerator; import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerCreationEventGenerator; @@ -40,7 +42,7 @@ void distributeMessagesTest() throws AlertException { private static final long serialVersionUID = 5355069038110415471L; }; IssueTrackerModelExtractor modelExtractor = new IssueTrackerModelExtractor<>(createFormatter(), null); - IssueTrackerAsyncMessageSender messageSender = createMessageSender(); + IssueTrackerAsyncMessageSender> messageSender = createMessageSender(); IssueTrackerProcessor processor = new IssueTrackerProcessor<>(modelExtractor, messageSender); IssueTrackerProcessorFactory processorFactory = (x, y, z) -> processor; @@ -60,17 +62,16 @@ void distributeMessagesTest() throws AlertException { assertEquals(processorResponse.getStatusMessage(), testResult.getStatusMessage()); } - private IssueTrackerAsyncMessageSender createMessageSender() { + private IssueTrackerAsyncMessageSender> createMessageSender() { IssueTrackerCommentEventGenerator commenter = (model) -> null; IssueTrackerTransitionEventGenerator transitioner = (model) -> null; IssueTrackerCreationEventGenerator creator = (model) -> null; RabbitTemplate rabbitTemplate = Mockito.mock(RabbitTemplate.class); EventManager eventManager = new EventManager(BlackDuckServicesFactory.createDefaultGson(), rabbitTemplate, new SyncTaskExecutor()); ExecutingJobManager executingJobManager = Mockito.mock(ExecutingJobManager.class); + DefaultIssueTrackerEventGenerator eventGenerator = new DefaultIssueTrackerEventGenerator<>(creator, transitioner, commenter); return new IssueTrackerAsyncMessageSender<>( - creator, - transitioner, - commenter, + eventGenerator, eventManager, UUID.randomUUID(), Set.of(1L, 2L, 3L), diff --git a/api-channel-issue-tracker/src/test/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerProcessorTest.java b/api-channel-issue-tracker/src/test/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerProcessorTest.java index 53e5aa723a..468bb2b773 100644 --- a/api-channel-issue-tracker/src/test/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerProcessorTest.java +++ b/api-channel-issue-tracker/src/test/java/com/blackduck/integration/alert/api/channel/issue/tracker/IssueTrackerProcessorTest.java @@ -31,7 +31,7 @@ void processMessagesTest() throws AlertException { Mockito.when(extractor.extractSimpleMessageIssueModels(Mockito.anyList(), Mockito.any())).thenReturn(simpleMessageResponses); IssueTrackerModelHolder projectMessageResponses = new IssueTrackerModelHolder<>(List.of(), List.of(), List.of()); - IssueTrackerAsyncMessageSender sender = Mockito.mock(IssueTrackerAsyncMessageSender.class); + IssueTrackerAsyncMessageSender> sender = Mockito.mock(IssueTrackerAsyncMessageSender.class); AtomicInteger messageCounter = new AtomicInteger(0); Mockito.doAnswer(invocation -> { messageCounter.incrementAndGet(); diff --git a/api-channel-issue-tracker/src/test/java/com/blackduck/integration/alert/api/channel/issue/tracker/action/IssueTrackerFieldModelTestActionTest.java b/api-channel-issue-tracker/src/test/java/com/blackduck/integration/alert/api/channel/issue/tracker/action/IssueTrackerFieldModelTestActionTest.java index 2e499b8315..36a5023e6e 100644 --- a/api-channel-issue-tracker/src/test/java/com/blackduck/integration/alert/api/channel/issue/tracker/action/IssueTrackerFieldModelTestActionTest.java +++ b/api-channel-issue-tracker/src/test/java/com/blackduck/integration/alert/api/channel/issue/tracker/action/IssueTrackerFieldModelTestActionTest.java @@ -21,7 +21,7 @@ import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerIssueResponseModel; import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerModelHolder; import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTransitionModel; -import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerAsyncMessageSender; +import com.blackduck.integration.alert.api.channel.issue.tracker.send.AsyncMessageSender; import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerMessageSender; import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerMessageSenderFactory; import com.blackduck.integration.alert.api.common.model.exception.AlertException; @@ -50,14 +50,14 @@ void testConfigSendMessagesThrowsException() throws AlertException { IssueTrackerMessageSender messageSender = Mockito.mock(IssueTrackerMessageSender.class); Mockito.when(messageSender.sendMessages(Mockito.any())).thenThrow(new AlertException(testExceptionMessage)); - IssueTrackerMessageSenderFactory messageSenderFactory = new IssueTrackerMessageSenderFactory<>() { + IssueTrackerMessageSenderFactory> messageSenderFactory = new IssueTrackerMessageSenderFactory<>() { @Override public IssueTrackerMessageSender createMessageSender(TestJobDetails distributionDetails, UUID globalId) throws AlertException { return messageSender; } @Override - public IssueTrackerAsyncMessageSender createAsyncMessageSender( + public AsyncMessageSender> createAsyncMessageSender( TestJobDetails distributionDetails, UUID globalId, UUID jobExecutionId, @@ -78,14 +78,14 @@ void testConfigSendMessagesReturnsNoIssues() throws AlertException { IssueTrackerMessageSender messageSender = Mockito.mock(IssueTrackerMessageSender.class); Mockito.when(messageSender.sendMessages(Mockito.any())).thenReturn(List.of()); - IssueTrackerMessageSenderFactory messageSenderFactory = new IssueTrackerMessageSenderFactory<>() { + IssueTrackerMessageSenderFactory> messageSenderFactory = new IssueTrackerMessageSenderFactory<>() { @Override public IssueTrackerMessageSender createMessageSender(TestJobDetails distributionDetails, UUID globalId) throws AlertException { return messageSender; } @Override - public IssueTrackerAsyncMessageSender createAsyncMessageSender( + public AsyncMessageSender> createAsyncMessageSender( TestJobDetails distributionDetails, UUID globalId, UUID jobExecutionId, @@ -106,14 +106,14 @@ void testConfigNoResolveTransition() throws AlertException { IssueTrackerMessageSender messageSender = Mockito.mock(IssueTrackerMessageSender.class); Mockito.when(messageSender.sendMessages(Mockito.any())).thenReturn(List.of(TEST_ISSUE_RESPONSE_MODEL)); - IssueTrackerMessageSenderFactory messageSenderFactory = new IssueTrackerMessageSenderFactory<>() { + IssueTrackerMessageSenderFactory> messageSenderFactory = new IssueTrackerMessageSenderFactory<>() { @Override public IssueTrackerMessageSender createMessageSender(TestJobDetails distributionDetails, UUID globalId) throws AlertException { return messageSender; } @Override - public IssueTrackerAsyncMessageSender createAsyncMessageSender( + public AsyncMessageSender> createAsyncMessageSender( TestJobDetails distributionDetails, UUID globalId, UUID jobExecutionId, @@ -141,14 +141,14 @@ void testConfigResolveFailure() throws AlertException { return List.of(); }); - IssueTrackerMessageSenderFactory messageSenderFactory = new IssueTrackerMessageSenderFactory<>() { + IssueTrackerMessageSenderFactory> messageSenderFactory = new IssueTrackerMessageSenderFactory<>() { @Override public IssueTrackerMessageSender createMessageSender(TestJobDetails distributionDetails, UUID globalId) throws AlertException { return messageSender; } @Override - public IssueTrackerAsyncMessageSender createAsyncMessageSender( + public AsyncMessageSender> createAsyncMessageSender( TestJobDetails distributionDetails, UUID globalId, UUID jobExecutionId, @@ -169,14 +169,14 @@ void testConfigNoReopen() throws AlertException { IssueTrackerMessageSender messageSender = Mockito.mock(IssueTrackerMessageSender.class); Mockito.when(messageSender.sendMessages(Mockito.any())).thenReturn(List.of(TEST_ISSUE_RESPONSE_MODEL)); - IssueTrackerMessageSenderFactory messageSenderFactory = new IssueTrackerMessageSenderFactory<>() { + IssueTrackerMessageSenderFactory> messageSenderFactory = new IssueTrackerMessageSenderFactory<>() { @Override public IssueTrackerMessageSender createMessageSender(TestJobDetails distributionDetails, UUID globalId) throws AlertException { return messageSender; } @Override - public IssueTrackerAsyncMessageSender createAsyncMessageSender( + public AsyncMessageSender> createAsyncMessageSender( TestJobDetails distributionDetails, UUID globalId, UUID jobExecutionId, @@ -209,14 +209,14 @@ void testConfigReopenFailure() throws AlertException { return List.of(TEST_ISSUE_RESPONSE_MODEL); }); - IssueTrackerMessageSenderFactory messageSenderFactory = new IssueTrackerMessageSenderFactory<>() { + IssueTrackerMessageSenderFactory> messageSenderFactory = new IssueTrackerMessageSenderFactory<>() { @Override public IssueTrackerMessageSender createMessageSender(TestJobDetails distributionDetails, UUID globalId) throws AlertException { return messageSender; } @Override - public IssueTrackerAsyncMessageSender createAsyncMessageSender( + public AsyncMessageSender> createAsyncMessageSender( TestJobDetails distributionDetails, UUID globalId, UUID jobExecutionId, @@ -254,11 +254,15 @@ private static class TestIssueTrackerTestAction extends IssueTrackerTestAction messageSenderFactory) { + public TestIssueTrackerTestAction(IssueTrackerMessageSenderFactory> messageSenderFactory) { this(messageSenderFactory, false, false); } - public TestIssueTrackerTestAction(IssueTrackerMessageSenderFactory messageSenderFactory, boolean hasResolveTransition, boolean hasReopenTransition) { + public TestIssueTrackerTestAction( + IssueTrackerMessageSenderFactory> messageSenderFactory, + boolean hasResolveTransition, + boolean hasReopenTransition + ) { super(ISSUE_TRACKER_KEY, messageSenderFactory); this.hasResolveTransition = hasResolveTransition; this.hasReopenTransition = hasReopenTransition; diff --git a/api-channel-issue-tracker/src/test/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/IssueTrackerAsyncMessageSenderTest.java b/api-channel-issue-tracker/src/test/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/IssueTrackerAsyncMessageSenderTest.java index a38e9b86c2..403f48c27f 100644 --- a/api-channel-issue-tracker/src/test/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/IssueTrackerAsyncMessageSenderTest.java +++ b/api-channel-issue-tracker/src/test/java/com/blackduck/integration/alert/api/channel/issue/tracker/send/IssueTrackerAsyncMessageSenderTest.java @@ -46,10 +46,9 @@ void sendAsyncMessagesNoEventsTest() { IssueTrackerCreationEventGenerator createEventGenerator = (model) -> null; IssueTrackerTransitionEventGenerator transitionEventGenerator = (model) -> null; IssueTrackerCommentEventGenerator commentEventGenerator = (model) -> null; - IssueTrackerAsyncMessageSender sender = new IssueTrackerAsyncMessageSender<>( - createEventGenerator, - transitionEventGenerator, - commentEventGenerator, + DefaultIssueTrackerEventGenerator eventGenerator = new DefaultIssueTrackerEventGenerator<>(createEventGenerator, transitionEventGenerator, commentEventGenerator); + IssueTrackerAsyncMessageSender> sender = new IssueTrackerAsyncMessageSender<>( + eventGenerator, mockEventManager, jobExecutionId, Set.of(1L, 2L, 3L), @@ -80,10 +79,9 @@ void sendAsyncMessageTest() { null ); IssueTrackerCommentEventGenerator commentEventGenerator = (model) -> new IssueTrackerCommentEvent<>(null, jobExecutionId, jobId, null, null); - IssueTrackerAsyncMessageSender sender = new IssueTrackerAsyncMessageSender<>( - createEventGenerator, - transitionEventGenerator, - commentEventGenerator, + DefaultIssueTrackerEventGenerator eventGenerator = new DefaultIssueTrackerEventGenerator<>(createEventGenerator, transitionEventGenerator, commentEventGenerator); + IssueTrackerAsyncMessageSender> sender = new IssueTrackerAsyncMessageSender<>( + eventGenerator, mockEventManager, jobExecutionId, Set.of(1L, 2L, 3L), diff --git a/channel-azure-boards/src/main/java/com/blackduck/integration/alert/channel/azure/boards/distribution/AzureBoardsMessageSenderFactory.java b/channel-azure-boards/src/main/java/com/blackduck/integration/alert/channel/azure/boards/distribution/AzureBoardsMessageSenderFactory.java index 90a099508a..acef9dfb29 100644 --- a/channel-azure-boards/src/main/java/com/blackduck/integration/alert/channel/azure/boards/distribution/AzureBoardsMessageSenderFactory.java +++ b/channel-azure-boards/src/main/java/com/blackduck/integration/alert/channel/azure/boards/distribution/AzureBoardsMessageSenderFactory.java @@ -14,7 +14,9 @@ import org.springframework.stereotype.Component; import com.blackduck.integration.alert.api.channel.issue.tracker.callback.IssueTrackerCallbackInfoCreator; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerModelHolder; import com.blackduck.integration.alert.api.channel.issue.tracker.search.IssueCategoryRetriever; +import com.blackduck.integration.alert.api.channel.issue.tracker.send.DefaultIssueTrackerEventGenerator; import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerAsyncMessageSender; import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerCommentEventGenerator; import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerCreationEventGenerator; @@ -50,7 +52,7 @@ import com.google.gson.Gson; @Component -public class AzureBoardsMessageSenderFactory implements IssueTrackerMessageSenderFactory { +public class AzureBoardsMessageSenderFactory implements IssueTrackerMessageSenderFactory> { private final Gson gson; private final IssueTrackerCallbackInfoCreator callbackInfoCreator; private final AzureBoardsChannelKey channelKey; @@ -112,7 +114,7 @@ public IssueTrackerMessageSender createMessageSender(AzureBoardsJobDeta } @Override - public IssueTrackerAsyncMessageSender createAsyncMessageSender( + public IssueTrackerAsyncMessageSender> createAsyncMessageSender( AzureBoardsJobDetailsModel distributionDetails, UUID globalId, UUID jobExecutionId, Set notificationIds @@ -165,7 +167,7 @@ public IssueTrackerMessageSender createMessageSender( ); } - public IssueTrackerAsyncMessageSender createAsyncMessageSender( + public IssueTrackerAsyncMessageSender> createAsyncMessageSender( AzureBoardsJobDetailsModel distributionDetails, UUID jobExecutionId, Set notificationIds @@ -179,11 +181,10 @@ public IssueTrackerAsyncMessageSender createAsyncMessageSender( jobId, notificationIds ); + DefaultIssueTrackerEventGenerator eventGenerator = new DefaultIssueTrackerEventGenerator<>(createEventGenerator, transitionEventGenerator, commentEventGenerator); return new IssueTrackerAsyncMessageSender<>( - createEventGenerator, - transitionEventGenerator, - commentEventGenerator, + eventGenerator, eventManager, jobExecutionId, notificationIds, diff --git a/channel-azure-boards/src/main/java/com/blackduck/integration/alert/channel/azure/boards/distribution/AzureBoardsProcessorFactory.java b/channel-azure-boards/src/main/java/com/blackduck/integration/alert/channel/azure/boards/distribution/AzureBoardsProcessorFactory.java index e6998b53c8..1dd8e8ec04 100644 --- a/channel-azure-boards/src/main/java/com/blackduck/integration/alert/channel/azure/boards/distribution/AzureBoardsProcessorFactory.java +++ b/channel-azure-boards/src/main/java/com/blackduck/integration/alert/channel/azure/boards/distribution/AzureBoardsProcessorFactory.java @@ -21,6 +21,7 @@ import com.blackduck.integration.alert.api.channel.issue.tracker.IssueTrackerProcessor; import com.blackduck.integration.alert.api.channel.issue.tracker.IssueTrackerProcessorFactory; import com.blackduck.integration.alert.api.channel.issue.tracker.convert.ProjectMessageToIssueModelTransformer; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerModelHolder; import com.blackduck.integration.alert.api.channel.issue.tracker.search.IssueCategoryRetriever; import com.blackduck.integration.alert.api.channel.issue.tracker.search.IssueTrackerSearcher; import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerAsyncMessageSender; @@ -134,7 +135,7 @@ public IssueTrackerProcessor createProcessor(AzureBoardsJobDetailsModel IssueTrackerModelExtractor extractor = new IssueTrackerModelExtractor<>(formatter, azureBoardsSearcher); - IssueTrackerAsyncMessageSender messageSender = azureBoardsMessageSenderFactory.createAsyncMessageSender( + IssueTrackerAsyncMessageSender> messageSender = azureBoardsMessageSenderFactory.createAsyncMessageSender( distributionDetails, null, jobExecutionId, diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/JiraCloudProcessor.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/JiraCloudProcessor.java new file mode 100644 index 0000000000..8045333ba7 --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/JiraCloudProcessor.java @@ -0,0 +1,46 @@ +/* + * blackduck-alert + * + * Copyright (c) 2025 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud; + +import java.util.LinkedList; +import java.util.List; + +import com.blackduck.integration.alert.api.channel.issue.tracker.IssueTrackerMessageProcessor; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerModelHolder; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerResponse; +import com.blackduck.integration.alert.api.channel.issue.tracker.send.AsyncMessageSender; +import com.blackduck.integration.alert.api.common.model.exception.AlertException; +import com.blackduck.integration.alert.api.processor.extract.model.ProviderMessageHolder; +import com.blackduck.integration.alert.api.processor.extract.model.project.ProjectMessage; +import com.blackduck.integration.alert.channel.jira.cloud.convert.JiraCloudModelExtractor; + +public class JiraCloudProcessor implements IssueTrackerMessageProcessor { + private final JiraCloudModelExtractor modelExtractor; + private final AsyncMessageSender> messageSender; + + public JiraCloudProcessor(JiraCloudModelExtractor modelExtractor, AsyncMessageSender> messageSender) { + this.modelExtractor = modelExtractor; + this.messageSender = messageSender; + } + + @Override + public IssueTrackerResponse processMessages(ProviderMessageHolder messages, String jobName) throws AlertException { + List> issueTrackerModels = new LinkedList<>(); + IssueTrackerModelHolder simpleMessageHolder = modelExtractor.extractSimpleMessageIssueModels(messages.getSimpleMessages(), jobName); + issueTrackerModels.add(simpleMessageHolder); + + for (ProjectMessage projectMessage : messages.getProjectMessages()) { + IssueTrackerModelHolder projectMessageHolder = modelExtractor.extractProjectMessageIssueModels(projectMessage, jobName); + issueTrackerModels.add(projectMessageHolder); + } + + messageSender.sendAsyncMessages(issueTrackerModels); + + return new IssueTrackerResponse<>("Success", List.of()); + } +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/AtlassianDocumentBuilder.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/AtlassianDocumentBuilder.java new file mode 100644 index 0000000000..dc971047cc --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/AtlassianDocumentBuilder.java @@ -0,0 +1,268 @@ +/* + * blackduck-alert + * + * Copyright (c) 2025 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud.convert; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; + +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.blackduck.integration.alert.api.channel.convert.ChannelMessageFormatter; +import com.blackduck.integration.alert.channel.jira.cloud.convert.model.AtlassianBulletList; +import com.blackduck.integration.alert.channel.jira.cloud.convert.model.AtlassianDocumentFormatNode; +import com.blackduck.integration.alert.channel.jira.cloud.convert.model.AtlassianDocumentFormatRootNode; +import com.blackduck.integration.alert.channel.jira.cloud.convert.model.AtlassianDocumentNode; +import com.blackduck.integration.alert.channel.jira.cloud.convert.model.AtlassianListItem; +import com.blackduck.integration.alert.channel.jira.cloud.convert.model.AtlassianParagraphContentNode; +import com.blackduck.integration.alert.channel.jira.cloud.convert.model.AtlassianTextContentNode; +import com.blackduck.integration.alert.common.message.model.LinkableItem; +import com.blackduck.integration.jira.common.cloud.builder.AtlassianDocumentFormatModelBuilder; +import com.blackduck.integration.jira.common.cloud.model.AtlassianDocumentFormatModel; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.micrometer.common.util.StringUtils; + +/** + * This class is used to build an {@link AtlassianDocumentFormatModel} object. + * This class maintains state for the latest document and latest paragraph in order to be able to create a document + * Once the document exceeds the size limit a new document is created and added to the list of documents for comment. + * A new paragraph is also created in order to allow additional text nodes to be added. + */ +public class AtlassianDocumentBuilder { + public static final Integer MAX_SERIALIZED_LENGTH = 30000; + public static final String DESCRIPTION_CONTINUED_TEXT = "(description continued...)"; + public static final AtlassianTextContentNode descriptionContinuedNode = new AtlassianTextContentNode(DESCRIPTION_CONTINUED_TEXT); + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final ObjectMapper objectMapper; + private final ChannelMessageFormatter formatter; + private final boolean descriptionDocument; + + // state variables + private final AtlassianDocumentNode primaryNode; + private final List additionalCommentNodes; + private AtlassianDocumentNode currentDocumentNode; + private AtlassianParagraphContentNode currentParagraph; + private AtlassianBulletList currentBulletList; + + private Integer currentDocumentLength; + private Integer computedMaximumDocumentLength; + + public AtlassianDocumentBuilder(ChannelMessageFormatter formatter, boolean descriptionDocument) { + this.objectMapper = new ObjectMapper(); + this.additionalCommentNodes = new ArrayList<>(); + this.formatter = formatter; + + // initialize an empty document with a single paragraph + this.primaryNode = new AtlassianDocumentNode(); + this.currentDocumentNode = primaryNode; + this.descriptionDocument = descriptionDocument; + initializeMaximumDocumentLength(); + initializeDocumentLength(); + initializeNewParagraph(); + } + + // private methods to initialize new nodes in the document + private void initializeMaximumDocumentLength() { + Integer descriptionContinuedLength = computeJsonStringLength(DESCRIPTION_CONTINUED_TEXT); + computedMaximumDocumentLength = MAX_SERIALIZED_LENGTH - descriptionContinuedLength; + } + + private void initializeNewDocument() { + this.currentDocumentNode = new AtlassianDocumentNode(); + initializeDocumentLength(); + additionalCommentNodes.add(this.currentDocumentNode); + } + + private void initializeNewParagraph() { + if (currentBulletList != null) { + finishBulletList(); + startBulletList(); + addListItem(); + } else { + this.currentParagraph = new AtlassianParagraphContentNode(); + this.currentDocumentNode.addContent(currentParagraph); + } + } + + private void initializeDocumentLength() { + currentDocumentLength = computeJsonStringLength(currentDocumentNode); + } + + private boolean willExceedLimit(Object object) { + int newObjectLength = computeJsonStringLength(object); + return this.currentDocumentLength + newObjectLength > computedMaximumDocumentLength; + } + + private int computeJsonStringLength(Object object) { + try { + String json = objectMapper.writeValueAsString(object); + return json.length(); + } catch (JsonProcessingException ex) { + logger.error("Error while initializing document length", ex); + } + return 0; + } + + public AtlassianDocumentBuilder addParagraphNode() { + AtlassianParagraphContentNode paragraphNode = new AtlassianParagraphContentNode(); + boolean willExceed = willExceedLimit(paragraphNode); + if (willExceed) { + initializeNewDocument(); + } + + this.currentParagraph = paragraphNode; + this.currentDocumentNode.addContent(paragraphNode); + if (willExceed) { + addDescriptionContinuedText(); + } + this.currentDocumentLength = computeJsonStringLength(currentDocumentNode); + return this; + } + + public AtlassianDocumentBuilder addTextNode(String text) { + return addTextNode(text, false); + } + + public AtlassianDocumentBuilder addTextNode(String text, boolean bold) { + AtlassianTextContentNode textNode = new AtlassianTextContentNode(text); + return addTextNode(textNode, bold, null); + } + + public AtlassianDocumentBuilder addTextNode(LinkableItem linkableItem, boolean bold) { + String label = formatter.encode(linkableItem.getLabel()); + String value = formatter.encode(linkableItem.getValue()); + String href = linkableItem.getUrl().map(formatter::encode).orElse(null); + String text = String.format("%s:%s", label, formatter.getNonBreakingSpace()); + AtlassianTextContentNode textNode = new AtlassianTextContentNode(text); + addTextNode(textNode, bold, null); + textNode = new AtlassianTextContentNode(value); + return addTextNode(textNode, bold, href); + } + + public AtlassianDocumentBuilder addTextNode(String text, String href) { + AtlassianTextContentNode textNode = new AtlassianTextContentNode(text); + return addTextNode(textNode, false, href); + } + + public AtlassianDocumentBuilder addTextNode(AtlassianTextContentNode textNode, boolean bold, @Nullable String href) { + if (bold) { + textNode.addBoldStyle(); + } + + if (StringUtils.isNotBlank(href)) { + textNode.addLink(href); + } + + if (willExceedLimit(textNode)) { + initializeNewDocument(); + initializeNewParagraph(); + addDescriptionContinuedText(); + } + + this.currentParagraph.addContent(textNode); + this.currentDocumentLength = computeJsonStringLength(currentDocumentNode); + return this; + } + + public AtlassianDocumentBuilder startBulletList() { + currentBulletList = new AtlassianBulletList(); + + if (willExceedLimit(currentBulletList)) { + initializeNewDocument(); + } + currentDocumentNode.addContent(currentBulletList); + this.currentDocumentLength = computeJsonStringLength(currentDocumentNode); + return this; + } + + public AtlassianDocumentBuilder addListItem() { + if (currentBulletList == null) { + throw new IllegalStateException("Current bullet list is empty"); + } + AtlassianListItem listItem = new AtlassianListItem(); + AtlassianParagraphContentNode paragraphNode = new AtlassianParagraphContentNode(); + listItem.addContent(paragraphNode); + + if (willExceedLimit(listItem)) { + initializeNewDocument(); + currentDocumentNode.addContent(currentBulletList); + } + currentBulletList.addContent(listItem); + this.currentParagraph = paragraphNode; + this.currentDocumentLength = computeJsonStringLength(currentDocumentNode); + return this; + } + + public AtlassianDocumentBuilder finishBulletList() { + if (currentBulletList == null) { + throw new IllegalStateException("Current bullet list is empty"); + } + currentBulletList = null; + addParagraphNode(); + return this; + } + + private void addDescriptionContinuedText() { + if (descriptionDocument) { + this.currentParagraph.addContent(descriptionContinuedNode); + } + } + + // This build will create a primary document for the i.e. for the description an if the document exceeds the limit then additional comments will need to be added. + public AtlassianDocumentFormatModel buildPrimaryDocument() { + return buildDocumentModel(primaryNode); + } + + // This build will create a list of documents that are used to add comments to an issue because the content has exceeded the limit + public List buildAdditionalCommentDocuments() { + List documents = new ArrayList<>(additionalCommentNodes.size()); + + for (AtlassianDocumentNode node : additionalCommentNodes) { + documents.add(buildDocumentModel(node)); + } + + return documents; + } + + // methods to convert the objects in this builder to the AtlassianDocumentFormatModel object + private AtlassianDocumentFormatModel buildDocumentModel(AtlassianDocumentNode documentNode) { + AtlassianDocumentFormatModelBuilder builder = new AtlassianDocumentFormatModelBuilder(); + + documentNode.getContent().stream() + .map(this::createRootContentNode) + .forEach(nodeData -> builder.addContentNode(nodeData.getLeft(), nodeData.getRight())); + return builder.build(); + } + + private Pair>> createRootContentNode(AtlassianDocumentFormatNode rootNode) { + if (rootNode instanceof final AtlassianDocumentFormatRootNode node) { + String type = node.getType(); + List> content = node.getContent().stream() + .map(this::createContentNode) + .toList(); + return Pair.of(type, content); + } + + // should not get to this point. + return Pair.of("", List.of()); + } + + private Map createContentNode(T textContentNode) { + return objectMapper.convertValue( + textContentNode, new TypeReference<>() { + } + ); + } +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudBomComponentDetailConverter.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudBomComponentDetailConverter.java new file mode 100644 index 0000000000..9a3c968a5c --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudBomComponentDetailConverter.java @@ -0,0 +1,84 @@ +/* + * blackduck-alert + * + * Copyright (c) 2025 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud.convert; + +import java.util.List; + +import com.blackduck.integration.alert.api.channel.convert.ChannelMessageFormatter; +import com.blackduck.integration.alert.api.processor.extract.model.project.AbstractBomComponentDetails; +import com.blackduck.integration.alert.api.processor.extract.model.project.ComponentUpgradeGuidance; +import com.blackduck.integration.alert.common.message.model.LinkableItem; + +public class JiraCloudBomComponentDetailConverter { + private final ChannelMessageFormatter formatter; + + public JiraCloudBomComponentDetailConverter(ChannelMessageFormatter formatter) { + this.formatter = formatter; + } + + public void gatherAbstractBomComponentSectionPieces(AbstractBomComponentDetails bomComponent, AtlassianDocumentBuilder documentBuilder) { + documentBuilder.addTextNode(bomComponent.getComponent(), true) + .addTextNode(formatter.getLineSeparator()); + + bomComponent.getComponentVersion().ifPresent(version -> documentBuilder.addParagraphNode() + .addTextNode(version, true) + .addParagraphNode()); + + gatherAttributeStrings(bomComponent, documentBuilder); + } + + public void gatherAttributeStrings(AbstractBomComponentDetails bomComponent, AtlassianDocumentBuilder documentBuilder) { + LinkableItem licenseItem = bomComponent.getLicense(); + String usageText = bomComponent.getUsage(); + ComponentUpgradeGuidance componentUpgradeGuidance = bomComponent.getComponentUpgradeGuidance(); + List additionalAttributes = bomComponent.getAdditionalAttributes(); + + String licenseString = formatAttributeLabel(licenseItem); + documentBuilder.startBulletList() + .addListItem() + .addTextNode(licenseString) + .addTextNode(formatter.encode(licenseItem.getValue()), licenseItem.getUrl().map(formatter::encode).orElse(null)) + .addTextNode(formatter.getLineSeparator()); + + LinkableItem usageItem = new LinkableItem("Usage", usageText); + String usageString = formatAttributeLabel(usageItem); + documentBuilder + .addListItem() + .addTextNode(usageString) + .addTextNode(formatter.encode(usageItem.getValue())) + .addTextNode(formatter.getLineSeparator()); + + componentUpgradeGuidance.getShortTermUpgradeGuidance() + .ifPresent(attr -> documentBuilder + .addListItem() + .addTextNode(formatAttributeLabel(attr)) + .addTextNode(formatter.encode(attr.getValue()), attr.getUrl().map(formatter::encode).orElse(null)) + .addTextNode(formatter.getLineSeparator())); + componentUpgradeGuidance.getLongTermUpgradeGuidance() + .ifPresent(attr -> documentBuilder + .addListItem() + .addTextNode(formatAttributeLabel(attr)) + .addTextNode(formatter.encode(attr.getValue()), attr.getUrl().map(formatter::encode).orElse(null)) + .addTextNode(formatter.getLineSeparator())); + + additionalAttributes + .forEach(attr -> documentBuilder + .addListItem() + .addTextNode(formatAttributeLabel(attr)) + .addTextNode(formatter.encode(attr.getValue()), attr.getUrl().map(formatter::encode).orElse(null)) + .addTextNode(formatter.getLineSeparator())); + + documentBuilder.finishBulletList(); + } + + private String formatAttributeLabel(LinkableItem linkableItem) { + String label = formatter.encode(linkableItem.getLabel()); + String formattedValue = String.format("%s:%s", label, formatter.getNonBreakingSpace()); + return String.format("%s%s", formatter.getNonBreakingSpace(), formattedValue); + } +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudComponentVulnerabilitiesConverter.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudComponentVulnerabilitiesConverter.java new file mode 100644 index 0000000000..df396d26e6 --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudComponentVulnerabilitiesConverter.java @@ -0,0 +1,90 @@ +/* + * blackduck-alert + * + * Copyright (c) 2024 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud.convert; + +import java.util.List; +import java.util.Optional; + +import com.blackduck.integration.alert.api.channel.convert.ChannelMessageFormatter; +import com.blackduck.integration.alert.api.processor.extract.model.project.ComponentVulnerabilities; +import com.blackduck.integration.alert.common.message.model.LinkableItem; + +public class JiraCloudComponentVulnerabilitiesConverter { + private static final String LABEL_VULNERABILITIES_SECTION = "Current Vulnerabilities: "; + private static final String VALUE_NO_CURRENT_VULNERABILITIES = "None"; + + private static final String LABEL_CRITICAL = "Critical"; + private static final String LABEL_HIGH = "High"; + private static final String LABEL_MEDIUM = "Medium"; + private static final String LABEL_LOW = "Low"; + + private final ChannelMessageFormatter formatter; + + private final String encodedLabelVulnerabilitiesSection; + private final String encodedValueNoCurrentVulnerabilities; + private final String encodedLabelCritical; + private final String encodedLabelHigh; + private final String encodedLabelMedium; + private final String encodedLabelLow; + + private final String encodedLeftBracket; + private final String encodedRightBracket; + + public JiraCloudComponentVulnerabilitiesConverter(ChannelMessageFormatter formatter) { + this.formatter = formatter; + + this.encodedLabelVulnerabilitiesSection = formatter.encode(LABEL_VULNERABILITIES_SECTION); + this.encodedValueNoCurrentVulnerabilities = formatter.encode(VALUE_NO_CURRENT_VULNERABILITIES); + this.encodedLabelCritical = createEncodedLabel(formatter, LABEL_CRITICAL); + this.encodedLabelHigh = createEncodedLabel(formatter, LABEL_HIGH); + this.encodedLabelMedium = createEncodedLabel(formatter, LABEL_MEDIUM); + this.encodedLabelLow = createEncodedLabel(formatter, LABEL_LOW); + + this.encodedLeftBracket = formatter.encode("[ "); + this.encodedRightBracket = formatter.encode(" ] "); + } + + public void createComponentVulnerabilitiesSectionPieces(ComponentVulnerabilities componentVulnerabilities, AtlassianDocumentBuilder documentBuilder) { + documentBuilder.addTextNode(encodedLabelVulnerabilitiesSection); + + if (componentVulnerabilities.hasVulnerabilities()) { + documentBuilder.startBulletList(); + createSeveritySection(documentBuilder, encodedLabelCritical, componentVulnerabilities.getCritical()); + createSeveritySection(documentBuilder, encodedLabelHigh, componentVulnerabilities.getHigh()); + createSeveritySection(documentBuilder, encodedLabelMedium, componentVulnerabilities.getMedium()); + createSeveritySection(documentBuilder, encodedLabelLow, componentVulnerabilities.getLow()); + documentBuilder.finishBulletList(); + } else { + documentBuilder.addTextNode(encodedValueNoCurrentVulnerabilities); + } + } + + private void createSeveritySection(AtlassianDocumentBuilder documentBuilder, String encodedLabel, List vulnerabilities) { + if (vulnerabilities.isEmpty()) { + return; + } + documentBuilder.addListItem(); + documentBuilder.addTextNode(encodedLabel); + + vulnerabilities + .forEach(item -> convertVulnerabilityToString(documentBuilder, item)); + } + + private void convertVulnerabilityToString(AtlassianDocumentBuilder documentBuilder, LinkableItem vulnerability) { + Optional url = vulnerability.getUrl(); + + String encodedUrl = url.orElse(null); + String text = String.format("%s%s%s", encodedLeftBracket, formatter.encode(vulnerability.getValue()), encodedRightBracket); + documentBuilder.addTextNode(text, encodedUrl); + } + + private static String createEncodedLabel(ChannelMessageFormatter formatter, String label) { + return String.format("%s%s:%s", formatter.getNonBreakingSpace(), label, formatter.getNonBreakingSpace()); + } + +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudIssueComponentUnknownVersionDetailsConverter.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudIssueComponentUnknownVersionDetailsConverter.java new file mode 100644 index 0000000000..993c68756b --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudIssueComponentUnknownVersionDetailsConverter.java @@ -0,0 +1,58 @@ +/* + * blackduck-alert + * + * Copyright (c) 2024 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud.convert; + +import com.blackduck.integration.alert.api.channel.convert.ChannelMessageFormatter; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueComponentUnknownVersionDetails; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueEstimatedRiskModel; +import com.blackduck.integration.alert.common.enumeration.ItemOperation; + +public class JiraCloudIssueComponentUnknownVersionDetailsConverter { + private static final String TEXT_COMPONENT_DELETE = "Component was removed or the version was set."; + private static final String SECTION_LABEL_VULNERABILITY_COUNTS = "Vulnerability counts:"; + private final ChannelMessageFormatter formatter; + + public JiraCloudIssueComponentUnknownVersionDetailsConverter(ChannelMessageFormatter formatter) { + this.formatter = formatter; + } + + public void createEstimatedRiskDetailsSectionPieces(IssueComponentUnknownVersionDetails unknownVersionDetails, AtlassianDocumentBuilder documentBuilder) { + + if (ItemOperation.DELETE.equals(unknownVersionDetails.getItemOperation())) { + documentBuilder.addTextNode(formatter.encode(TEXT_COMPONENT_DELETE)); + documentBuilder.addTextNode(formatter.getLineSeparator()); + } else { + documentBuilder.addTextNode(formatter.encode(SECTION_LABEL_VULNERABILITY_COUNTS)); + + documentBuilder.startBulletList(); + for (IssueEstimatedRiskModel estimatedRiskModel : unknownVersionDetails.getEstimatedRiskModelList()) { + createEstimatedRiskString(estimatedRiskModel, documentBuilder); + } + documentBuilder.finishBulletList(); + } + } + + private void createEstimatedRiskString(IssueEstimatedRiskModel estimatedRiskModel, AtlassianDocumentBuilder documentBuilder) { + documentBuilder.addListItem(); + String severity = formatter.encode(estimatedRiskModel.getSeverity().getVulnerabilityLabel()); + String countString = formatter.encode(String.format("(%s)", estimatedRiskModel.getCount())); + String componentName = formatter.encode(estimatedRiskModel.getName()); + // " : () " + final String builder = formatter.getNonBreakingSpace() + + formatter.getNonBreakingSpace() + + formatter.getNonBreakingSpace() + + formatter.getNonBreakingSpace() + + severity + + formatter.encode(":") + + formatter.getNonBreakingSpace() + + countString + + formatter.getNonBreakingSpace(); + documentBuilder.addTextNode(builder) + .addTextNode(componentName, estimatedRiskModel.getComponentVersionUrl().map(formatter::encode).orElse(null)); + } +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudIssuePolicyDetailsConverter.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudIssuePolicyDetailsConverter.java new file mode 100644 index 0000000000..af3b12af68 --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudIssuePolicyDetailsConverter.java @@ -0,0 +1,80 @@ +/* + * blackduck-alert + * + * Copyright (c) 2024 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud.convert; + +import java.util.LinkedList; +import java.util.List; + +import com.blackduck.integration.alert.api.channel.convert.ChannelMessageFormatter; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueBomComponentDetails; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssuePolicyDetails; +import com.blackduck.integration.alert.api.processor.extract.model.project.ComponentPolicy; + +public class JiraCloudIssuePolicyDetailsConverter { + private static final String LABEL_POLICY = "Policy: "; + private static final String LABEL_SEVERITY = "Severity: "; + private static final String LABEL_DESCRIPTION = "Policy Description: "; + + private final ChannelMessageFormatter formatter; + private final JiraCloudComponentVulnerabilitiesConverter componentVulnerabilitiesConverter; + + public JiraCloudIssuePolicyDetailsConverter(ChannelMessageFormatter formatter) { + this.formatter = formatter; + this.componentVulnerabilitiesConverter = new JiraCloudComponentVulnerabilitiesConverter(formatter); + } + + public void createPolicyDetailsSectionPieces(IssueBomComponentDetails bomComponentDetails, IssuePolicyDetails policyDetails, AtlassianDocumentBuilder documentBuilder) { + + documentBuilder.addTextNode(formatter.encode(LABEL_POLICY)) + .addTextNode(formatter.encode(policyDetails.getName())) + .addTextNode(formatter.getLineSeparator()) + .addTextNode(formatter.encode(LABEL_SEVERITY)) + .addTextNode(formatter.encode(policyDetails.getSeverity().getPolicyLabel())); + + List policyDescriptionSection = createPolicyDescription(bomComponentDetails, policyDetails); + policyDescriptionSection.forEach(documentBuilder::addTextNode); + + boolean isVulnerabilityPolicy = bomComponentDetails.getRelevantPolicies() + .stream() + .filter(policy -> policy.getPolicyName().equals(policyDetails.getName())) + .anyMatch(ComponentPolicy::isVulnerabilityPolicy); + + if (isVulnerabilityPolicy) { + documentBuilder.addTextNode(formatter.getLineSeparator()) + .addTextNode(formatter.getSectionSeparator()) + .addParagraphNode() + .addTextNode(formatter.getLineSeparator()); + componentVulnerabilitiesConverter.createComponentVulnerabilitiesSectionPieces(bomComponentDetails.getComponentVulnerabilities(), documentBuilder); + } + } + + private List createPolicyDescription(IssueBomComponentDetails bomComponentDetails, IssuePolicyDetails policyDetails) { + List policies = bomComponentDetails.getRelevantPolicies(); + if (policies.isEmpty()) { + return List.of(); + } + + String policyName = policyDetails.getName(); + // Blackduck does not allow duplicate policy names, we only expect one policy to ever match + return policies.stream() + .filter(policy -> policyName.equals(policy.getPolicyName())) + .findAny() + .flatMap(ComponentPolicy::getDescription) + .map(this::addPolicyDescriptionPieces) + .orElse(List.of()); + } + + private List addPolicyDescriptionPieces(String description) { + List descriptionPieces = new LinkedList<>(); + descriptionPieces.add(formatter.getLineSeparator()); + descriptionPieces.add(formatter.encode(LABEL_DESCRIPTION)); + descriptionPieces.add(formatter.encode(description)); + return descriptionPieces; + } + +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudIssueTrackerSimpleMessageConverter.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudIssueTrackerSimpleMessageConverter.java new file mode 100644 index 0000000000..b98a0a584f --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudIssueTrackerSimpleMessageConverter.java @@ -0,0 +1,41 @@ +/* + * blackduck-alert + * + * Copyright (c) 2024 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud.convert; + +import java.util.List; + +import org.apache.commons.lang3.StringUtils; + +import com.blackduck.integration.alert.api.channel.issue.tracker.convert.IssueTrackerMessageFormatter; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueCreationModel; +import com.blackduck.integration.alert.api.processor.extract.model.SimpleMessage; +import com.blackduck.integration.alert.common.message.model.LinkableItem; +import com.blackduck.integration.jira.common.cloud.model.AtlassianDocumentFormatModel; + +public class JiraCloudIssueTrackerSimpleMessageConverter { + private final IssueTrackerMessageFormatter formatter; + private final JiraCloudSimpleMessageConverter simpleMessageConverter; + + public JiraCloudIssueTrackerSimpleMessageConverter(IssueTrackerMessageFormatter formatter) { + this.formatter = formatter; + this.simpleMessageConverter = new JiraCloudSimpleMessageConverter(formatter); + } + + public IssueCreationModel convertToIssueCreationModel(SimpleMessage simpleMessage, String jobName, AtlassianDocumentBuilder documentBuilder) { + LinkableItem provider = simpleMessage.getProvider(); + String rawTitle = String.format("%s[%s] | %s", provider.getLabel(), provider.getValue(), simpleMessage.getSummary()); + String truncatedTitle = StringUtils.truncate(rawTitle, formatter.getMaxTitleLength()); + + simpleMessageConverter.convertToFormattedMessageChunks(simpleMessage, jobName, documentBuilder); + + AtlassianDocumentFormatModel description = documentBuilder.buildPrimaryDocument(); + List additionalComments = documentBuilder.buildAdditionalCommentDocuments(); + return IssueCreationModel.simple(truncatedTitle, simpleMessage.getProvider(), description, additionalComments); + } + +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudIssueVulnerabilityDetailsConverter.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudIssueVulnerabilityDetailsConverter.java new file mode 100644 index 0000000000..cb5e84bf43 --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudIssueVulnerabilityDetailsConverter.java @@ -0,0 +1,73 @@ +/* + * blackduck-alert + * + * Copyright (c) 2024 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud.convert; + +import java.util.Collection; +import java.util.Optional; + +import com.blackduck.integration.alert.api.channel.issue.tracker.convert.IssueTrackerMessageFormatter; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueVulnerabilityDetails; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueVulnerabilityModel; +import com.blackduck.integration.alert.api.processor.extract.model.project.ComponentConcernSeverity; +import com.blackduck.integration.alert.common.message.model.LinkableItem; + +public class JiraCloudIssueVulnerabilityDetailsConverter { + private static final String SECTION_LABEL_VULNERABILITIES = "Vulnerabilities: "; + private static final String OP_PARTICIPLE_ADDED = "Added: "; + private static final String OP_PARTICIPLE_UPDATED = "Updated: "; + private static final String OP_PARTICIPLE_DELETED = "Deleted: "; + private static final String LABEL_SEVERITY = "Severity: "; + + private final IssueTrackerMessageFormatter formatter; + + public JiraCloudIssueVulnerabilityDetailsConverter(IssueTrackerMessageFormatter formatter) { + this.formatter = formatter; + } + + public void createVulnerabilityDetailsSectionPieces(IssueVulnerabilityDetails vulnerabilityDetails, AtlassianDocumentBuilder documentBuilder) { + documentBuilder.addTextNode(formatter.encode(SECTION_LABEL_VULNERABILITIES)); + + createIssueVulnerabilityCollectionSectionPieces(OP_PARTICIPLE_ADDED, vulnerabilityDetails.getVulnerabilitiesAdded(), documentBuilder); + createIssueVulnerabilityCollectionSectionPieces(OP_PARTICIPLE_UPDATED, vulnerabilityDetails.getVulnerabilitiesUpdated(), documentBuilder); + createIssueVulnerabilityCollectionSectionPieces(OP_PARTICIPLE_DELETED, vulnerabilityDetails.getVulnerabilitiesDeleted(), documentBuilder); + } + + private void createIssueVulnerabilityCollectionSectionPieces( + String operationParticiple, + Collection vulnerabilities, + AtlassianDocumentBuilder documentBuilder + ) { + documentBuilder.addTextNode(formatter.getLineSeparator()); + documentBuilder.addTextNode(formatter.encode(operationParticiple)); + + String encodedSeverityPrefix = formatter.encode(LABEL_SEVERITY); + ComponentConcernSeverity currentSeverity = ComponentConcernSeverity.UNSPECIFIED_UNKNOWN; + + for (IssueVulnerabilityModel vulnerability : vulnerabilities) { + ComponentConcernSeverity vulnerabilitySeverity = vulnerability.getSeverity(); + if (!currentSeverity.equals(vulnerabilitySeverity)) { + currentSeverity = vulnerabilitySeverity; + + documentBuilder.addTextNode(formatter.getLineSeparator()); + documentBuilder.addTextNode(encodedSeverityPrefix); + documentBuilder.addTextNode(formatter.encode(currentSeverity.getVulnerabilityLabel())); + documentBuilder.addTextNode(formatter.getLineSeparator()); + } + + LinkableItem vulnerabilityItem = vulnerability.getVulnerability(); + Optional optionalUrl = vulnerabilityItem.getUrl().map(formatter::encode); + String encodedValue = formatter.encode(vulnerabilityItem.getValue()); + + String vulnerabilityDetail = String.format("[%s%s%s]", formatter.getNonBreakingSpace(), encodedValue, formatter.getNonBreakingSpace()); + documentBuilder.addTextNode(vulnerabilityDetail, optionalUrl.orElse(null)); + + documentBuilder.addTextNode(formatter.getNonBreakingSpace()); + } + } + +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudModelExtractor.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudModelExtractor.java new file mode 100644 index 0000000000..c8eafb8b1a --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudModelExtractor.java @@ -0,0 +1,93 @@ +/* + * blackduck-alert + * + * Copyright (c) 2025 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud.convert; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import com.blackduck.integration.alert.api.channel.issue.tracker.convert.IssueTrackerMessageFormatter; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueCommentModel; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueCreationModel; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerModelHolder; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTransitionModel; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.ProjectIssueModel; +import com.blackduck.integration.alert.api.channel.issue.tracker.search.ActionableIssueSearchResult; +import com.blackduck.integration.alert.api.channel.issue.tracker.search.ExistingIssueDetails; +import com.blackduck.integration.alert.api.channel.issue.tracker.search.IssueTrackerSearcher; +import com.blackduck.integration.alert.api.common.model.exception.AlertException; +import com.blackduck.integration.alert.api.processor.extract.model.SimpleMessage; +import com.blackduck.integration.alert.api.processor.extract.model.project.ProjectMessage; +import com.blackduck.integration.alert.common.enumeration.ItemOperation; + +public class JiraCloudModelExtractor { + private final JiraCloudIssueTrackerSimpleMessageConverter issueTrackerSimpleMessageConverter; + private final JiraCloudProjectIssueModelConverter projectIssueModelConverter; + private final IssueTrackerSearcher issueTrackerSearcher; + private final IssueTrackerMessageFormatter formatter; + + public JiraCloudModelExtractor(IssueTrackerMessageFormatter formatter, IssueTrackerSearcher issueTrackerSearcher) { + this.formatter = formatter; + this.issueTrackerSimpleMessageConverter = new JiraCloudIssueTrackerSimpleMessageConverter(formatter); + this.projectIssueModelConverter = new JiraCloudProjectIssueModelConverter(formatter); + this.issueTrackerSearcher = issueTrackerSearcher; + } + + public final IssueTrackerModelHolder extractSimpleMessageIssueModels(List simpleMessages, String jobName) { + List simpleMessageIssueCreationModels = new ArrayList<>(simpleMessages.size()); + for (SimpleMessage simpleMessage : simpleMessages) { + AtlassianDocumentBuilder documentBuilder = new AtlassianDocumentBuilder(formatter, true); + IssueCreationModel simpleMessageIssueCreationModel = issueTrackerSimpleMessageConverter.convertToIssueCreationModel(simpleMessage, jobName, documentBuilder); + simpleMessageIssueCreationModels.add(simpleMessageIssueCreationModel); + } + + return new IssueTrackerModelHolder<>(simpleMessageIssueCreationModels, List.of(), List.of()); + } + + public final IssueTrackerModelHolder extractProjectMessageIssueModels(ProjectMessage projectMessage, String jobName) throws AlertException { + IssueTrackerModelHolder combinedResults = new IssueTrackerModelHolder<>(List.of(), List.of(), List.of()); + List> searchResults = issueTrackerSearcher.findIssues(projectMessage); + for (ActionableIssueSearchResult searchResult : searchResults) { + IssueTrackerModelHolder searchResultMessages = convertSearchResult(searchResult, jobName); + combinedResults = IssueTrackerModelHolder.reduce(combinedResults, searchResultMessages); + } + return combinedResults; + } + + private IssueTrackerModelHolder convertSearchResult(ActionableIssueSearchResult searchResult, String jobName) { + Optional> existingIssueDetails = searchResult.getExistingIssueDetails(); + ProjectIssueModel projectIssueModel = searchResult.getProjectIssueModel(); + if (existingIssueDetails.isPresent()) { + return convertExistingIssue(existingIssueDetails.get(), projectIssueModel, searchResult.getRequiredOperation()); + } else { + IssueCreationModel issueCreationModel = projectIssueModelConverter.toIssueCreationModel(projectIssueModel, jobName, searchResult.getSearchQuery()); + return new IssueTrackerModelHolder<>(List.of(issueCreationModel), List.of(), List.of()); + } + } + + private IssueTrackerModelHolder convertExistingIssue( + ExistingIssueDetails existingIssueDetails, + ProjectIssueModel projectIssueModel, + ItemOperation requiredOperation + ) { + List> transitionModels = new ArrayList<>(1); + List> commentModels = new ArrayList<>(1); + if (ItemOperation.UPDATE.equals(requiredOperation) || ItemOperation.INFO.equals(requiredOperation)) { + IssueCommentModel projectIssueCommentModel = projectIssueModelConverter.toIssueCommentModel(existingIssueDetails, projectIssueModel); + commentModels.add(projectIssueCommentModel); + } else { + IssueTransitionModel projectIssueTransitionModel = projectIssueModelConverter.toIssueTransitionModel( + existingIssueDetails, + projectIssueModel, + requiredOperation + ); + transitionModels.add(projectIssueTransitionModel); + } + return new IssueTrackerModelHolder<>(List.of(), transitionModels, commentModels); + } +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudProjectIssueModelConverter.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudProjectIssueModelConverter.java new file mode 100644 index 0000000000..1d88f7285c --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudProjectIssueModelConverter.java @@ -0,0 +1,255 @@ +/* + * blackduck-alert + * + * Copyright (c) 2024 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud.convert; + +import java.io.Serializable; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; + +import org.apache.commons.lang3.StringUtils; + +import com.blackduck.integration.alert.api.channel.issue.tracker.convert.IssueTrackerMessageFormatter; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueBomComponentDetails; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueCommentModel; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueComponentUnknownVersionDetails; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueCreationModel; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssuePolicyDetails; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTransitionModel; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueVulnerabilityDetails; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.ProjectIssueModel; +import com.blackduck.integration.alert.api.channel.issue.tracker.search.ExistingIssueDetails; +import com.blackduck.integration.alert.api.processor.extract.model.project.ComponentConcernSeverity; +import com.blackduck.integration.alert.api.processor.extract.model.project.ComponentConcernType; +import com.blackduck.integration.alert.api.processor.extract.model.project.ComponentVulnerabilities; +import com.blackduck.integration.alert.common.channel.issuetracker.enumeration.IssueOperation; +import com.blackduck.integration.alert.common.channel.message.ChunkedStringBuilder; +import com.blackduck.integration.alert.common.enumeration.ItemOperation; +import com.blackduck.integration.alert.common.message.model.LinkableItem; +import com.blackduck.integration.jira.common.cloud.model.AtlassianDocumentFormatModel; + +public class JiraCloudProjectIssueModelConverter { + public static final int COMPONENT_CONCERN_TITLE_SECTION_CHAR_COUNT = 20; + public static final LinkableItem MISSING_PROJECT_VERSION_PLACEHOLDER = new LinkableItem("Project Version", "Unknown"); + public static final String DESCRIPTION_CONTINUED_TEXT = "(description continued...)"; + public static final String COMMA_SPACE = ", "; + public static final String LABEL_SEVERITY_STATUS = "Severity Status: "; + + private final IssueTrackerMessageFormatter formatter; + private final JiraCloudBomComponentDetailConverter bomComponentDetailConverter; + private final JiraCloudIssuePolicyDetailsConverter issuePolicyDetailsConverter; + private final JiraCloudIssueVulnerabilityDetailsConverter issueVulnerabilityDetailsConverter; + private final JiraCloudIssueComponentUnknownVersionDetailsConverter issueComponentUnknownVersionDetailsConverter; + private final JiraCloudComponentVulnerabilitiesConverter componentVulnerabilitiesConverter; + + public JiraCloudProjectIssueModelConverter(IssueTrackerMessageFormatter formatter) { + this.formatter = formatter; + this.bomComponentDetailConverter = new JiraCloudBomComponentDetailConverter(formatter); + this.issuePolicyDetailsConverter = new JiraCloudIssuePolicyDetailsConverter(formatter); + this.issueVulnerabilityDetailsConverter = new JiraCloudIssueVulnerabilityDetailsConverter(formatter); + this.issueComponentUnknownVersionDetailsConverter = new JiraCloudIssueComponentUnknownVersionDetailsConverter(formatter); + this.componentVulnerabilitiesConverter = new JiraCloudComponentVulnerabilitiesConverter(formatter); + } + + public IssueCreationModel toIssueCreationModel(ProjectIssueModel projectIssueModel, String jobName, String queryString) { + String title = createTruncatedTitle(projectIssueModel); + AtlassianDocumentBuilder documentBuilder = new AtlassianDocumentBuilder(formatter, true); + ChunkedStringBuilder descriptionBuilder = new ChunkedStringBuilder(formatter.getMaxDescriptionLength()); + + String nonBreakingSpace = formatter.getNonBreakingSpace(); + String jobLine = String.format("Job%sname:%s%s", nonBreakingSpace, nonBreakingSpace, jobName); + documentBuilder.addTextNode(jobLine, true) + .addParagraphNode() + .addTextNode(projectIssueModel.getProject(), true) + .addParagraphNode() + .addTextNode(projectIssueModel.getProjectVersion().orElse(MISSING_PROJECT_VERSION_PLACEHOLDER), true) + .addParagraphNode() + .addTextNode(formatter.getSectionSeparator(), false) + .addParagraphNode(); + + IssueBomComponentDetails bomComponent = projectIssueModel.getBomComponentDetails(); + bomComponentDetailConverter.gatherAbstractBomComponentSectionPieces(bomComponent, documentBuilder); + + createVulnerabilitySeverityStatusSectionPieces(projectIssueModel, documentBuilder); + createProjectIssueModelConcernSectionPieces(projectIssueModel, documentBuilder, false); + + AtlassianDocumentFormatModel description = documentBuilder.buildPrimaryDocument(); + List additionalComments = documentBuilder.buildAdditionalCommentDocuments(); + + return IssueCreationModel.project(title, "", List.of(), projectIssueModel, description, additionalComments, queryString); + } + + public IssueTransitionModel toIssueTransitionModel( + ExistingIssueDetails existingIssueDetails, + ProjectIssueModel projectIssueModel, + ItemOperation requiredOperation + ) { + IssueOperation issueOperation; + if (ItemOperation.ADD.equals(requiredOperation)) { + issueOperation = IssueOperation.OPEN; + } else { + issueOperation = IssueOperation.RESOLVE; + } + + IssueCommentModel commentModel = toIssueCommentModel(existingIssueDetails, projectIssueModel); + List transitionComments = new LinkedList<>(commentModel.getComments()); + + LinkableItem provider = projectIssueModel.getProvider(); + ChunkedStringBuilder commentBuilder = new ChunkedStringBuilder(formatter.getMaxCommentLength()); + commentBuilder.append(String.format("The %s operation was performed on this component in %s.", requiredOperation.name(), provider.getLabel())); + + transitionComments.addAll(commentBuilder.collectCurrentChunks()); + + return new IssueTransitionModel<>(existingIssueDetails, issueOperation, transitionComments, projectIssueModel); + } + + public IssueCommentModel toIssueCommentModel(ExistingIssueDetails existingIssueDetails, ProjectIssueModel projectIssueModel) { + AtlassianDocumentBuilder documentBuilder = new AtlassianDocumentBuilder(formatter, false); + + LinkableItem provider = projectIssueModel.getProvider(); + String commentHeader = String.format("The component was updated in %s[%s]", provider.getLabel(), provider.getValue()); + documentBuilder + .addTextNode(formatter.encode(commentHeader)) + .addParagraphNode() + .addTextNode(formatter.getSectionSeparator()) + .addParagraphNode(); + + createVulnerabilitySeverityStatusSectionPieces(projectIssueModel, documentBuilder); + + createProjectIssueModelConcernSectionPieces(projectIssueModel, documentBuilder, true); + + IssueBomComponentDetails bomComponent = projectIssueModel.getBomComponentDetails(); + bomComponentDetailConverter.gatherAttributeStrings(bomComponent, documentBuilder); + + AtlassianDocumentFormatModel primaryComment = documentBuilder.buildPrimaryDocument(); + List additionalComments = documentBuilder.buildAdditionalCommentDocuments(); + return new IssueCommentModel<>(existingIssueDetails, List.of(), projectIssueModel, primaryComment, additionalComments); + } + + private String createTruncatedTitle(ProjectIssueModel projectIssueModel) { + LinkableItem provider = projectIssueModel.getProvider(); + LinkableItem project = projectIssueModel.getProject(); + LinkableItem projectVersion = projectIssueModel.getProjectVersion().orElse(MISSING_PROJECT_VERSION_PLACEHOLDER); + + IssueBomComponentDetails bomComponent = projectIssueModel.getBomComponentDetails(); + LinkableItem component = bomComponent.getComponent(); + Optional optionalComponentVersionValue = bomComponent.getComponentVersion().map(LinkableItem::getValue); + boolean isComponentVersionUnknown = projectIssueModel.getComponentUnknownVersionDetails().isPresent(); + StringBuilder componentPieceBuilder = new StringBuilder(); + componentPieceBuilder.append(component.getValue()); + + if (optionalComponentVersionValue.isPresent() && !isComponentVersionUnknown) { + componentPieceBuilder.append('['); + componentPieceBuilder.append(optionalComponentVersionValue.get()); + componentPieceBuilder.append(']'); + } + + StringBuilder componentConcernPieceBuilder = new StringBuilder(); + + Optional optionalPolicyName = projectIssueModel.getPolicyDetails().map(IssuePolicyDetails::getName); + if (optionalPolicyName.isPresent()) { + componentConcernPieceBuilder.append(COMMA_SPACE); + componentConcernPieceBuilder.append(ComponentConcernType.POLICY.getDisplayName()); + componentConcernPieceBuilder.append('['); + componentConcernPieceBuilder.append(optionalPolicyName.get()); + componentConcernPieceBuilder.append(']'); + } else if (isComponentVersionUnknown) { + componentConcernPieceBuilder.append(COMMA_SPACE); + componentConcernPieceBuilder.append(ComponentConcernType.UNKNOWN_VERSION.getDisplayName()); + } else { + componentConcernPieceBuilder.append(COMMA_SPACE); + componentConcernPieceBuilder.append(ComponentConcernType.VULNERABILITY.getDisplayName()); + } + + String componentConcernPiece = componentConcernPieceBuilder.toString(); + + String preConcernTitle = String.format( + "Alert - %s[%s], %s[%s], %s", + provider.getLabel(), + provider.getValue(), + project.getValue(), + projectVersion.getValue(), + componentPieceBuilder + ); + if (preConcernTitle.length() + componentConcernPieceBuilder.length() > formatter.getMaxTitleLength()) { + if (formatter.getMaxTitleLength() > COMPONENT_CONCERN_TITLE_SECTION_CHAR_COUNT) { + preConcernTitle = StringUtils.truncate(preConcernTitle, formatter.getMaxTitleLength() - COMPONENT_CONCERN_TITLE_SECTION_CHAR_COUNT); + componentConcernPiece = StringUtils.truncate(componentConcernPieceBuilder.toString(), COMPONENT_CONCERN_TITLE_SECTION_CHAR_COUNT); + } else { + // If max title length is less than 3, then there are bigger concerns than an IllegalArgumentException + preConcernTitle = StringUtils.truncate(preConcernTitle, formatter.getMaxTitleLength() - 3); + componentConcernPiece = "..."; + } + } + + return preConcernTitle + componentConcernPiece; + } + + private void createProjectIssueModelConcernSectionPieces(ProjectIssueModel projectIssueModel, AtlassianDocumentBuilder documentBuilder, boolean commentFormat) { + + IssueBomComponentDetails bomComponentDetails = projectIssueModel.getBomComponentDetails(); + + Optional optionalPolicyDetails = projectIssueModel.getPolicyDetails(); + if (optionalPolicyDetails.isPresent()) { + issuePolicyDetailsConverter.createPolicyDetailsSectionPieces(bomComponentDetails, optionalPolicyDetails.get(), documentBuilder); + documentBuilder + .addTextNode(formatter.getLineSeparator()) + .addTextNode(formatter.getSectionSeparator()) + .addTextNode(formatter.getLineSeparator()); + } + + Optional optionalVulnDetails = projectIssueModel.getVulnerabilityDetails(); + if (optionalVulnDetails.isPresent()) { + if (commentFormat) { + issueVulnerabilityDetailsConverter.createVulnerabilityDetailsSectionPieces(optionalVulnDetails.get(), documentBuilder); + } else { + componentVulnerabilitiesConverter.createComponentVulnerabilitiesSectionPieces( + projectIssueModel.getBomComponentDetails().getComponentVulnerabilities(), + documentBuilder + ); + } + + documentBuilder + .addTextNode(formatter.getLineSeparator()) + .addTextNode(formatter.getSectionSeparator()) + .addTextNode(formatter.getLineSeparator()); + } + + Optional optionalUnknownVersionDetails = projectIssueModel.getComponentUnknownVersionDetails(); + if (optionalUnknownVersionDetails.isPresent()) { + issueComponentUnknownVersionDetailsConverter.createEstimatedRiskDetailsSectionPieces(optionalUnknownVersionDetails.get(), documentBuilder); + + documentBuilder + .addTextNode(formatter.getLineSeparator()) + .addTextNode(formatter.getSectionSeparator()) + .addTextNode(formatter.getLineSeparator()); + } + + } + + private void createVulnerabilitySeverityStatusSectionPieces(ProjectIssueModel projectIssueModel, AtlassianDocumentBuilder documentBuilder) { + String encodedSeverityStatus = formatter.encode(LABEL_SEVERITY_STATUS); + IssueBomComponentDetails bomComponentDetails = projectIssueModel.getBomComponentDetails(); + + Optional vulnerabilityDetails = projectIssueModel.getVulnerabilityDetails(); + if (vulnerabilityDetails.isPresent()) { + ComponentVulnerabilities componentVulnerabilities = bomComponentDetails.getComponentVulnerabilities(); + componentVulnerabilities.computeHighestSeverity() + .map(ComponentConcernSeverity::getVulnerabilityLabel) + .map(formatter::encode) + .map(severity -> encodedSeverityStatus + severity) + .ifPresentOrElse(documentBuilder::addTextNode, () -> documentBuilder.addTextNode(encodedSeverityStatus + "None")); + documentBuilder + .addTextNode(formatter.getLineSeparator()) + .addTextNode(formatter.getSectionSeparator()) + .addTextNode(formatter.getLineSeparator()); + documentBuilder.addParagraphNode(); + } + } + +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudSimpleMessageConverter.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudSimpleMessageConverter.java new file mode 100644 index 0000000000..b582a889d5 --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/JiraCloudSimpleMessageConverter.java @@ -0,0 +1,50 @@ +/* + * blackduck-alert + * + * Copyright (c) 2025 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud.convert; + +import com.blackduck.integration.alert.api.channel.convert.ChannelMessageFormatter; +import com.blackduck.integration.alert.api.processor.extract.model.SimpleMessage; +import com.blackduck.integration.alert.common.message.model.LinkableItem; + +public class JiraCloudSimpleMessageConverter { + private final ChannelMessageFormatter messageFormatter; + + public JiraCloudSimpleMessageConverter(ChannelMessageFormatter messageFormatter) { + this.messageFormatter = messageFormatter; + } + + public void convertToFormattedMessageChunks(SimpleMessage simpleMessage, String jobName, AtlassianDocumentBuilder documentBuilder) { + + String nonBreakingSpace = messageFormatter.getNonBreakingSpace(); + String jobLine = String.format("Job%sname:%s%s", nonBreakingSpace, nonBreakingSpace, jobName); + String formattedJob = messageFormatter.emphasize(jobLine); + appendSection(formattedJob, documentBuilder); + appendSection(simpleMessage.getSummary(), documentBuilder); + appendSection(simpleMessage.getDescription(), documentBuilder); + + appendLinkableItem(documentBuilder, simpleMessage.getProvider(), false); + + for (LinkableItem detail : simpleMessage.getDetails()) { + appendLinkableItem(documentBuilder, detail, false); + } + } + + private void appendSection(String txt, AtlassianDocumentBuilder documentBuilder) { + String encodedTxt = messageFormatter.encode(txt); + documentBuilder.addTextNode(encodedTxt) + .addTextNode(messageFormatter.getLineSeparator()) + .addTextNode(messageFormatter.getSectionSeparator()) + .addTextNode(messageFormatter.getLineSeparator()) + .addParagraphNode(); + } + + private void appendLinkableItem(AtlassianDocumentBuilder documentBuilder, LinkableItem linkableItem, boolean bold) { + documentBuilder.addTextNode(linkableItem, bold) + .addTextNode(messageFormatter.getLineSeparator()); + } +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianBulletList.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianBulletList.java new file mode 100644 index 0000000000..8f9227c931 --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianBulletList.java @@ -0,0 +1,16 @@ +/* + * blackduck-alert + * + * Copyright (c) 2025 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud.convert.model; + +public class AtlassianBulletList extends AtlassianRootNode { + public static final String NODE_TYPE = "bulletList"; + + public AtlassianBulletList() { + super(NODE_TYPE); + } +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianDocumentFormatNode.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianDocumentFormatNode.java new file mode 100644 index 0000000000..5a000ad782 --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianDocumentFormatNode.java @@ -0,0 +1,14 @@ +/* + * blackduck-alert + * + * Copyright (c) 2025 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud.convert.model; + +public interface AtlassianDocumentFormatNode { + + String getType(); + +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianDocumentFormatRootNode.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianDocumentFormatRootNode.java new file mode 100644 index 0000000000..22c3b2fb60 --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianDocumentFormatRootNode.java @@ -0,0 +1,16 @@ +/* + * blackduck-alert + * + * Copyright (c) 2025 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud.convert.model; + +import java.util.List; + +public interface AtlassianDocumentFormatRootNode extends AtlassianDocumentFormatNode { + List getContent(); + + void addContent(AtlassianDocumentFormatNode content); +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianDocumentNode.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianDocumentNode.java new file mode 100644 index 0000000000..0ca2649400 --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianDocumentNode.java @@ -0,0 +1,30 @@ +/* + * blackduck-alert + * + * Copyright (c) 2025 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud.convert.model; + +import java.util.LinkedList; +import java.util.List; + +public class AtlassianDocumentNode extends AtlassianRootNode { + public static final String NODE_TYPE = "doc"; + public static final Integer DEFAULT_VERSION = 1; + private final Integer version; + + public AtlassianDocumentNode() { + this(NODE_TYPE, DEFAULT_VERSION, new LinkedList<>()); + } + + protected AtlassianDocumentNode(String type, Integer version, List content) { + super(type, content); + this.version = version; + } + + public Integer getVersion() { + return version; + } +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianListItem.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianListItem.java new file mode 100644 index 0000000000..05f89ccbea --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianListItem.java @@ -0,0 +1,16 @@ +/* + * blackduck-alert + * + * Copyright (c) 2025 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud.convert.model; + +public class AtlassianListItem extends AtlassianRootNode { + public static final String NODE_TYPE = "listItem"; + + public AtlassianListItem() { + super(NODE_TYPE); + } +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianParagraphContentNode.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianParagraphContentNode.java new file mode 100644 index 0000000000..a085e17ac4 --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianParagraphContentNode.java @@ -0,0 +1,18 @@ +/* + * blackduck-alert + * + * Copyright (c) 2025 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud.convert.model; + +import java.util.LinkedList; + +public class AtlassianParagraphContentNode extends AtlassianRootNode { + public static final String NODE_TYPE = "paragraph"; + + public AtlassianParagraphContentNode() { + super(NODE_TYPE, new LinkedList<>()); + } +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianRootNode.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianRootNode.java new file mode 100644 index 0000000000..a543c187b8 --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianRootNode.java @@ -0,0 +1,39 @@ +/* + * blackduck-alert + * + * Copyright (c) 2025 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud.convert.model; + +import java.io.Serializable; +import java.util.LinkedList; +import java.util.List; + +public class AtlassianRootNode implements AtlassianDocumentFormatRootNode, Serializable { + private final String type; + private final List content; + + public AtlassianRootNode(String type) { + this(type, new LinkedList<>()); + } + + public AtlassianRootNode(final String type, final List content) { + this.type = type; + this.content = content; + } + + public String getType() { + return type; + } + + public List getContent() { + return content; + } + + @Override + public void addContent(final AtlassianDocumentFormatNode content) { + this.content.add(content); + } +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianTextContentNode.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianTextContentNode.java new file mode 100644 index 0000000000..be12e7b035 --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/AtlassianTextContentNode.java @@ -0,0 +1,55 @@ +/* + * blackduck-alert + * + * Copyright (c) 2025 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud.convert.model; + +import java.io.Serializable; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.annotation.Nullable; + +public class AtlassianTextContentNode implements AtlassianDocumentFormatNode, Serializable { + public static final String NODE_TYPE = "text"; + private final String type; + private final String text; + private final Set marks; + + public AtlassianTextContentNode(String text) { + this(NODE_TYPE, text, new LinkedHashSet<>()); + } + + protected AtlassianTextContentNode(String type, String text, Set marks) { + this.type = type; + this.text = text; + this.marks = marks == null ? new LinkedHashSet<>() : marks; + } + + public String getType() { + return type; + } + + public String getText() { + return text; + } + + @Nullable + public Set getMarks() { + if (marks.isEmpty()) { + return null; + } + return marks; + } + + public void addBoldStyle() { + marks.add(new BoldMarkNode()); + } + + public void addLink(String href) { + marks.add(new LinkMarkNode(href)); + } +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/BoldMarkNode.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/BoldMarkNode.java new file mode 100644 index 0000000000..399c30596a --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/BoldMarkNode.java @@ -0,0 +1,30 @@ +/* + * blackduck-alert + * + * Copyright (c) 2025 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud.convert.model; + +import java.io.Serializable; + +import com.blackduck.integration.util.Stringable; + +public class BoldMarkNode extends Stringable implements AtlassianDocumentFormatNode, Serializable { + public static final String NODE_TYPE = "strong"; + private final String type; + + public BoldMarkNode() { + this(NODE_TYPE); + } + + protected BoldMarkNode(String type) { + this.type = type; + } + + @Override + public String getType() { + return type; + } +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/HrefNode.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/HrefNode.java new file mode 100644 index 0000000000..23a281ab49 --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/HrefNode.java @@ -0,0 +1,25 @@ +/* + * blackduck-alert + * + * Copyright (c) 2025 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud.convert.model; + +import java.io.Serializable; + +import com.blackduck.integration.util.Stringable; + +public class HrefNode extends Stringable implements Serializable { + private final String href; + + public HrefNode(String href) { + this.href = href; + } + + public String getHref() { + return href; + } + +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/LinkMarkNode.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/LinkMarkNode.java new file mode 100644 index 0000000000..4077ee19ed --- /dev/null +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/convert/model/LinkMarkNode.java @@ -0,0 +1,36 @@ +/* + * blackduck-alert + * + * Copyright (c) 2025 Black Duck Software, Inc. + * + * Use subject to the terms and conditions of the Black Duck Software End User Software License and Maintenance Agreement. All rights reserved worldwide. + */ +package com.blackduck.integration.alert.channel.jira.cloud.convert.model; + +import java.io.Serializable; + +import com.blackduck.integration.util.Stringable; + +public class LinkMarkNode extends Stringable implements AtlassianDocumentFormatNode, Serializable { + public static final String NODE_TYPE = "link"; + private final String type; + private final HrefNode attrs; + + public LinkMarkNode(String href) { + this(NODE_TYPE, new HrefNode(href)); + } + + protected LinkMarkNode(String type, HrefNode attrs) { + this.type = type; + this.attrs = attrs; + } + + @Override + public String getType() { + return type; + } + + public HrefNode getAttrs() { + return attrs; + } +} diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/distribution/JiraCloudMessageSenderFactory.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/distribution/JiraCloudMessageSenderFactory.java index 3c943026f7..babd8eb18e 100644 --- a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/distribution/JiraCloudMessageSenderFactory.java +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/distribution/JiraCloudMessageSenderFactory.java @@ -10,14 +10,16 @@ import java.util.Set; import java.util.UUID; -import com.blackduck.integration.jira.common.cloud.builder.IssueRequestModelFieldsBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.blackduck.integration.alert.api.channel.issue.tracker.callback.IssueTrackerCallbackInfoCreator; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerModelHolder; import com.blackduck.integration.alert.api.channel.issue.tracker.search.IssueCategoryRetriever; +import com.blackduck.integration.alert.api.channel.issue.tracker.send.AsyncMessageSender; +import com.blackduck.integration.alert.api.channel.issue.tracker.send.DefaultIssueTrackerEventGenerator; import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerAsyncMessageSender; import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerCommentEventGenerator; import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerCreationEventGenerator; @@ -42,6 +44,7 @@ import com.blackduck.integration.alert.channel.jira.cloud.distribution.delegate.JiraCloudIssueTransitioner; import com.blackduck.integration.alert.channel.jira.cloud.distribution.delegate.JiraCloudTransitionGenerator; import com.blackduck.integration.alert.common.persistence.model.job.details.JiraCloudJobDetailsModel; +import com.blackduck.integration.jira.common.cloud.builder.IssueRequestModelFieldsBuilder; import com.blackduck.integration.jira.common.cloud.service.FieldService; import com.blackduck.integration.jira.common.cloud.service.IssueSearchService; import com.blackduck.integration.jira.common.cloud.service.IssueService; @@ -51,7 +54,7 @@ import com.google.gson.Gson; @Component -public class JiraCloudMessageSenderFactory implements IssueTrackerMessageSenderFactory { +public class JiraCloudMessageSenderFactory implements IssueTrackerMessageSenderFactory> { private final Logger logger = LoggerFactory.getLogger(getClass()); private final Gson gson; @@ -113,7 +116,7 @@ public IssueTrackerMessageSender createMessageSender(JiraCloudJobDetails } @Override - public IssueTrackerAsyncMessageSender createAsyncMessageSender( + public AsyncMessageSender> createAsyncMessageSender( JiraCloudJobDetailsModel distributionDetails, UUID globalId, UUID jobExecutionId, @@ -158,7 +161,7 @@ public IssueTrackerMessageSender createMessageSender( ); } - public IssueTrackerAsyncMessageSender createAsyncMessageSender( + public AsyncMessageSender> createAsyncMessageSender( JiraCloudJobDetailsModel distributionDetails, UUID jobExecutionId, Set notificationIds @@ -167,11 +170,9 @@ public IssueTrackerAsyncMessageSender createAsyncMessageSender( IssueTrackerCommentEventGenerator commentEventGenerator = new JiraCloudCommentGenerator(channelKey, jobExecutionId, jobId, notificationIds); IssueTrackerCreationEventGenerator createEventGenerator = new JiraCloudCreateEventGenerator(channelKey, jobExecutionId, jobId, notificationIds); IssueTrackerTransitionEventGenerator transitionEventGenerator = new JiraCloudTransitionGenerator(channelKey, jobExecutionId, jobId, notificationIds); - + DefaultIssueTrackerEventGenerator eventGenerator = new DefaultIssueTrackerEventGenerator<>(createEventGenerator, transitionEventGenerator, commentEventGenerator); return new IssueTrackerAsyncMessageSender<>( - createEventGenerator, - transitionEventGenerator, - commentEventGenerator, + eventGenerator, eventManager, jobExecutionId, notificationIds, diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/distribution/JiraCloudProcessorFactory.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/distribution/JiraCloudProcessorFactory.java index e48f58964e..0bd19d848c 100644 --- a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/distribution/JiraCloudProcessorFactory.java +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/distribution/JiraCloudProcessorFactory.java @@ -16,13 +16,13 @@ import org.springframework.stereotype.Component; import com.blackduck.integration.alert.api.certificates.AlertSSLContextManager; -import com.blackduck.integration.alert.api.channel.issue.tracker.IssueTrackerModelExtractor; -import com.blackduck.integration.alert.api.channel.issue.tracker.IssueTrackerProcessor; +import com.blackduck.integration.alert.api.channel.issue.tracker.IssueTrackerMessageProcessor; import com.blackduck.integration.alert.api.channel.issue.tracker.IssueTrackerProcessorFactory; import com.blackduck.integration.alert.api.channel.issue.tracker.convert.ProjectMessageToIssueModelTransformer; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerModelHolder; import com.blackduck.integration.alert.api.channel.issue.tracker.search.IssueCategoryRetriever; import com.blackduck.integration.alert.api.channel.issue.tracker.search.IssueTrackerSearcher; -import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerAsyncMessageSender; +import com.blackduck.integration.alert.api.channel.issue.tracker.send.AsyncMessageSender; import com.blackduck.integration.alert.api.channel.jira.JiraConstants; import com.blackduck.integration.alert.api.channel.jira.distribution.JiraMessageFormatter; import com.blackduck.integration.alert.api.channel.jira.distribution.search.JiraIssueAlertPropertiesManager; @@ -31,7 +31,9 @@ import com.blackduck.integration.alert.api.common.model.exception.AlertConfigurationException; import com.blackduck.integration.alert.api.common.model.exception.AlertException; import com.blackduck.integration.alert.api.descriptor.JiraCloudChannelKey; +import com.blackduck.integration.alert.channel.jira.cloud.JiraCloudProcessor; import com.blackduck.integration.alert.channel.jira.cloud.JiraCloudProperties; +import com.blackduck.integration.alert.channel.jira.cloud.convert.JiraCloudModelExtractor; import com.blackduck.integration.alert.channel.jira.cloud.descriptor.JiraCloudDescriptor; import com.blackduck.integration.alert.common.channel.issuetracker.exception.IssueTrackerException; import com.blackduck.integration.alert.common.enumeration.ConfigContextEnum; @@ -86,7 +88,7 @@ public JiraCloudProcessorFactory( } @Override - public IssueTrackerProcessor createProcessor(JiraCloudJobDetailsModel distributionDetails, UUID jobExecutionId, Set notificationIds) + public IssueTrackerMessageProcessor createProcessor(JiraCloudJobDetailsModel distributionDetails, UUID jobExecutionId, Set notificationIds) throws AlertException { JiraCloudProperties jiraProperties = createJiraCloudProperties(); JiraCloudServiceFactory jiraCloudServiceFactory = jiraProperties.createJiraServicesCloudFactory(logger, gson); @@ -115,15 +117,14 @@ public IssueTrackerProcessor createProcessor(JiraCloudJobDetailsModel di JiraCloudQueryExecutor jiraCloudQueryExecutor = new JiraCloudQueryExecutor(issueSearchService); IssueTrackerSearcher jiraSearcher = jiraSearcherFactory.createJiraSearcher(distributionDetails.getProjectNameOrKey(), jiraCloudQueryExecutor); - IssueTrackerModelExtractor extractor = new IssueTrackerModelExtractor<>(jiraMessageFormatter, jiraSearcher); - - IssueTrackerAsyncMessageSender messageSender = messageSenderFactory.createAsyncMessageSender( + JiraCloudModelExtractor extractor = new JiraCloudModelExtractor(jiraMessageFormatter, jiraSearcher); + AsyncMessageSender> messageSender = messageSenderFactory.createAsyncMessageSender( distributionDetails, jobExecutionId, notificationIds ); - return new IssueTrackerProcessor<>(extractor, messageSender); + return new JiraCloudProcessor(extractor, messageSender); } private JiraCloudProperties createJiraCloudProperties() throws AlertConfigurationException { diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/distribution/delegate/JiraCloudIssueCommenter.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/distribution/delegate/JiraCloudIssueCommenter.java index adef210619..cdac1843f1 100644 --- a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/distribution/delegate/JiraCloudIssueCommenter.java +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/distribution/delegate/JiraCloudIssueCommenter.java @@ -7,15 +7,25 @@ */ package com.blackduck.integration.alert.channel.jira.cloud.distribution.delegate; +import java.util.List; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueCommentModel; import com.blackduck.integration.alert.api.channel.issue.tracker.search.ExistingIssueDetails; import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerIssueResponseCreator; import com.blackduck.integration.alert.api.channel.jira.distribution.delegate.JiraIssueCommenter; +import com.blackduck.integration.alert.api.common.model.exception.AlertException; import com.blackduck.integration.alert.common.persistence.model.job.details.JiraCloudJobDetailsModel; import com.blackduck.integration.exception.IntegrationException; -import com.blackduck.integration.jira.common.cloud.service.IssueService; +import com.blackduck.integration.jira.common.cloud.model.AtlassianDocumentFormatModel; import com.blackduck.integration.jira.common.cloud.model.IssueCommentRequestModel; +import com.blackduck.integration.jira.common.cloud.service.IssueService; public class JiraCloudIssueCommenter extends JiraIssueCommenter { + private final Logger logger = LoggerFactory.getLogger(getClass()); private final IssueService issueService; private final JiraCloudJobDetailsModel distributionDetails; @@ -39,4 +49,42 @@ protected void addComment(IssueCommentRequestModel requestModel) throws Integrat protected IssueCommentRequestModel createCommentModel(String comment, ExistingIssueDetails existingIssueDetails) throws IntegrationException { return IssueCommentRequestModel.commentForIssue(existingIssueDetails.getIssueKey(), comment); } + + @Override + protected void addComments(IssueCommentModel issueCommentModel) throws AlertException { + if (!isCommentingEnabled()) { + logger.debug(COMMENTING_DISABLED_MESSAGE); + return; + } + + ExistingIssueDetails existingIssueDetails = issueCommentModel.getExistingIssueDetails(); + try { + // need to create the model from the issue comment model which has the atlassian document format + Optional primaryComment = issueCommentModel.getAtlassianDocumentFormatCommentModel(); + IssueCommentRequestModel commentRequestModel; + if (primaryComment.isPresent()) { + commentRequestModel = new IssueCommentRequestModel(existingIssueDetails.getIssueKey(), primaryComment.get()); + addComment(commentRequestModel); + } + + // need to create the model from the issue comment model which has the atlassian document format + Optional> additionalComments = issueCommentModel.getAdditionalComments(); + if (additionalComments.isPresent()) { + List additionalCommentsList = additionalComments.get(); + for (AtlassianDocumentFormatModel additionalComment : additionalCommentsList) { + commentRequestModel = new IssueCommentRequestModel(existingIssueDetails.getIssueKey(), additionalComment); + addComment(commentRequestModel); + } + } + + List originalComments = issueCommentModel.getComments(); + if (originalComments != null && !originalComments.isEmpty()) { + for (String comment : originalComments) { + addComment(comment, existingIssueDetails, issueCommentModel.getSource().orElse(null)); + } + } + } catch (IntegrationException ex) { + throw new AlertException(String.format("Failed to add a comment in Jira. Issue Key: %s", existingIssueDetails.getIssueKey()), ex); + } + } } diff --git a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/distribution/delegate/JiraCloudIssueCreator.java b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/distribution/delegate/JiraCloudIssueCreator.java index c359abe91b..6f895f58a2 100644 --- a/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/distribution/delegate/JiraCloudIssueCreator.java +++ b/channel-jira-cloud/src/main/java/com/blackduck/integration/alert/channel/jira/cloud/distribution/delegate/JiraCloudIssueCreator.java @@ -7,14 +7,20 @@ */ package com.blackduck.integration.alert.channel.jira.cloud.distribution.delegate; +import java.util.LinkedList; import java.util.List; +import java.util.Optional; import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.blackduck.integration.alert.api.channel.issue.tracker.callback.IssueTrackerCallbackInfoCreator; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueCommentModel; import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueCreationModel; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.ProjectIssueModel; +import com.blackduck.integration.alert.api.channel.issue.tracker.search.ExistingIssueDetails; import com.blackduck.integration.alert.api.channel.issue.tracker.search.IssueCategoryRetriever; import com.blackduck.integration.alert.api.channel.jira.distribution.JiraErrorMessageUtility; import com.blackduck.integration.alert.api.channel.jira.distribution.JiraIssueCreationRequestCreator; @@ -29,6 +35,9 @@ import com.blackduck.integration.alert.channel.jira.cloud.distribution.JiraCloudQueryExecutor; import com.blackduck.integration.alert.common.persistence.model.job.details.JiraCloudJobDetailsModel; import com.blackduck.integration.exception.IntegrationException; +import com.blackduck.integration.jira.common.cloud.builder.AtlassianDocumentFormatModelBuilder; +import com.blackduck.integration.jira.common.cloud.builder.IssueRequestModelFieldsBuilder; +import com.blackduck.integration.jira.common.cloud.model.AtlassianDocumentFormatModel; import com.blackduck.integration.jira.common.cloud.model.IssueCreationRequestModel; import com.blackduck.integration.jira.common.cloud.service.IssueService; import com.blackduck.integration.jira.common.cloud.service.ProjectService; @@ -39,13 +48,14 @@ import com.blackduck.integration.jira.common.model.response.PageOfProjectsResponseModel; public class JiraCloudIssueCreator extends JiraIssueCreator { - private Logger logger = LoggerFactory.getLogger(getClass()); + private final Logger logger = LoggerFactory.getLogger(getClass()); private final JiraCloudJobDetailsModel distributionDetails; private final IssueService issueService; private final ProjectService projectService; private final JiraIssueCreationRequestCreator jiraIssueCreationRequestCreator; private final IssueCategoryRetriever issueCategoryRetriever; private final JiraCloudQueryExecutor jiraCloudQueryExecutor; + private final JiraCloudIssueCommenter jiraCloudIssueCommenter; public JiraCloudIssueCreator( JiraCloudChannelKey jiraCloudChannelKey, @@ -75,6 +85,7 @@ public JiraCloudIssueCreator( this.jiraIssueCreationRequestCreator = jiraIssueCreationRequestCreator; this.issueCategoryRetriever = issueCategoryRetriever; this.jiraCloudQueryExecutor = jiraCloudQueryExecutor; + this.jiraCloudIssueCommenter = jiraCloudIssueCommenter; } @Override @@ -109,6 +120,11 @@ protected IssueCreationRequestModel createIssueCreationRequest(IssueCreationMode replacementValues, distributionDetails.getCustomFields() ); + + // The description field is an empty string. The actual description comes from the atlassian model. Set the description field. + Optional description = alertIssueCreationModel.getAtlassianDocumentFormatDescriptionModel(); + description.ifPresent(atlassianDocumentFormatModel -> fieldsBuilder.setField(IssueRequestModelFieldsBuilder.DESCRIPTION, atlassianDocumentFormatModel)); + return new IssueCreationRequestModel( distributionDetails.getIssueCreatorEmail(), distributionDetails.getIssueType(), @@ -148,4 +164,18 @@ private ProjectComponent retrieveProjectComponent() throws AlertException { .orElseThrow(() -> new AlertException(String.format("Unable to find project matching '%s'", jiraProjectName))); } + @Override + protected void addPostCreateComments(ExistingIssueDetails issueDetails, IssueCreationModel creationModel, @Nullable ProjectIssueModel projectSource) + throws AlertException { + LinkedList postCreateComments = new LinkedList<>(); + AtlassianDocumentFormatModelBuilder atlassianDocumentFormatModelBuilder = new AtlassianDocumentFormatModelBuilder(); + atlassianDocumentFormatModelBuilder.addSingleParagraphTextNode("This issue was automatically created by Alert."); + AtlassianDocumentFormatModel firstComment = atlassianDocumentFormatModelBuilder.build(); + Optional> additionalCommentList = creationModel.getAtlassianDocumentFormatCommentModel(); + additionalCommentList.ifPresent(postCreateComments::addAll); + + IssueCommentModel commentRequestModel = new IssueCommentModel<>(issueDetails, List.of(), projectSource, firstComment, postCreateComments); + jiraCloudIssueCommenter.commentOnIssue(commentRequestModel); + } + } diff --git a/channel-jira-server/src/main/java/com/blackduck/integration/alert/channel/jira/server/distribution/JiraServerMessageSenderFactory.java b/channel-jira-server/src/main/java/com/blackduck/integration/alert/channel/jira/server/distribution/JiraServerMessageSenderFactory.java index 806dcebab2..fa9692f704 100644 --- a/channel-jira-server/src/main/java/com/blackduck/integration/alert/channel/jira/server/distribution/JiraServerMessageSenderFactory.java +++ b/channel-jira-server/src/main/java/com/blackduck/integration/alert/channel/jira/server/distribution/JiraServerMessageSenderFactory.java @@ -10,14 +10,15 @@ import java.util.Set; import java.util.UUID; -import com.blackduck.integration.jira.common.server.builder.IssueRequestModelFieldsBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.blackduck.integration.alert.api.channel.issue.tracker.callback.IssueTrackerCallbackInfoCreator; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerModelHolder; import com.blackduck.integration.alert.api.channel.issue.tracker.search.IssueCategoryRetriever; +import com.blackduck.integration.alert.api.channel.issue.tracker.send.DefaultIssueTrackerEventGenerator; import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerAsyncMessageSender; import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerCommentEventGenerator; import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerCreationEventGenerator; @@ -43,6 +44,7 @@ import com.blackduck.integration.alert.channel.jira.server.distribution.delegate.JiraServerTransitionGenerator; import com.blackduck.integration.alert.common.persistence.model.job.details.JiraServerJobDetailsModel; import com.blackduck.integration.jira.common.rest.service.IssuePropertyService; +import com.blackduck.integration.jira.common.server.builder.IssueRequestModelFieldsBuilder; import com.blackduck.integration.jira.common.server.service.FieldService; import com.blackduck.integration.jira.common.server.service.IssueSearchService; import com.blackduck.integration.jira.common.server.service.IssueService; @@ -51,7 +53,7 @@ import com.google.gson.Gson; @Component -public class JiraServerMessageSenderFactory implements IssueTrackerMessageSenderFactory { +public class JiraServerMessageSenderFactory implements IssueTrackerMessageSenderFactory> { private final Logger logger = LoggerFactory.getLogger(getClass()); private final Gson gson; @@ -116,7 +118,7 @@ public IssueTrackerMessageSender createMessageSender(JiraServerJobDetail } @Override - public IssueTrackerAsyncMessageSender createAsyncMessageSender( + public IssueTrackerAsyncMessageSender> createAsyncMessageSender( JiraServerJobDetailsModel distributionDetails, UUID globalId, UUID jobExecutionId, @@ -161,7 +163,7 @@ public IssueTrackerMessageSender createMessageSender( } - public IssueTrackerAsyncMessageSender createAsyncMessageSender( + public IssueTrackerAsyncMessageSender> createAsyncMessageSender( JiraServerJobDetailsModel distributionDetails, UUID jobExecutionId, Set notificationIds @@ -176,10 +178,10 @@ public IssueTrackerAsyncMessageSender createAsyncMessageSender( notificationIds ); + DefaultIssueTrackerEventGenerator eventGenerator = new DefaultIssueTrackerEventGenerator<>(createEventGenerator, transitionEventGenerator, commentEventGenerator); + return new IssueTrackerAsyncMessageSender<>( - createEventGenerator, - transitionEventGenerator, - commentEventGenerator, + eventGenerator, eventManager, jobExecutionId, notificationIds, diff --git a/channel-jira-server/src/main/java/com/blackduck/integration/alert/channel/jira/server/distribution/JiraServerProcessorFactory.java b/channel-jira-server/src/main/java/com/blackduck/integration/alert/channel/jira/server/distribution/JiraServerProcessorFactory.java index c05982a417..ba7022ee3a 100644 --- a/channel-jira-server/src/main/java/com/blackduck/integration/alert/channel/jira/server/distribution/JiraServerProcessorFactory.java +++ b/channel-jira-server/src/main/java/com/blackduck/integration/alert/channel/jira/server/distribution/JiraServerProcessorFactory.java @@ -19,6 +19,7 @@ import com.blackduck.integration.alert.api.channel.issue.tracker.IssueTrackerProcessor; import com.blackduck.integration.alert.api.channel.issue.tracker.IssueTrackerProcessorFactory; import com.blackduck.integration.alert.api.channel.issue.tracker.convert.ProjectMessageToIssueModelTransformer; +import com.blackduck.integration.alert.api.channel.issue.tracker.model.IssueTrackerModelHolder; import com.blackduck.integration.alert.api.channel.issue.tracker.search.IssueCategoryRetriever; import com.blackduck.integration.alert.api.channel.issue.tracker.search.IssueTrackerSearcher; import com.blackduck.integration.alert.api.channel.issue.tracker.send.IssueTrackerAsyncMessageSender; @@ -99,8 +100,8 @@ public IssueTrackerProcessor createProcessor(JiraServerJobDetailsModel d IssueTrackerSearcher jiraSearcher = jiraSearcherFactory.createJiraSearcher(distributionDetails.getProjectNameOrKey(), jiraServerQueryExecutor); IssueTrackerModelExtractor extractor = new IssueTrackerModelExtractor<>(jiraMessageFormatter, jiraSearcher); - - IssueTrackerAsyncMessageSender messageSender = jiraServerMessageSenderFactory.createAsyncMessageSender( + + IssueTrackerAsyncMessageSender> messageSender = jiraServerMessageSenderFactory.createAsyncMessageSender( distributionDetails, jobExecutionId, notificationIds