import com.atlassian.jira.component.ComponentAccessor import com.exalate.api.domain.connection.IConnection import com.exalate.api.domain.twintrace.ITwinTrace import com.exalate.api.persistence.twintrace.ITwinTraceRepository import com.exalate.basic.domain.hubobject.v1.BasicHubIssue import com.exalate.node.hubobject.v1_3.NodeHelper import com.exalate.services.utils.FutureUtils import org.slf4j.Logger import scala.collection.immutable.Seq /** Usage: Add the snippet below to the end of your "Outgoing sync": Epic.sendEpicFirst() // pollForEpicLinkUpdatesInterval = null -------------------------------- Add the snippet below to the end of your "Incoming sync": Epic.receive() -------------------------------- * */ class Epic { static log = org.slf4j.LoggerFactory.getLogger("com.exalate.scripts.Epic") static BasicHubIssue sendInAnyOrder(Long pollForEpicLinkUpdatesInterval = null) { return (new EpicCtx()).sendInAnyOrder(pollForEpicLinkUpdatesInterval) } static BasicHubIssue sendEpicFirst(Long pollForEpicLinkUpdatesInterval = null) { return (new EpicCtx()).sendEpicFirst() } static BasicHubIssue send() { return (new EpicCtx()).sendEpicFirst() } static BasicHubIssue receive() { return (new EpicCtx()).receive() } static def issueLevelError(String msg) { new com.exalate.api.exception.IssueTrackerException(msg) } static class EpicCtx { BasicHubIssue sendInAnyOrder(Long pollForEpicLinkUpdatesInterval) { send(pollForEpicLinkUpdatesInterval, { _, replica, issue, connection -> sendEpicInAnyOrder(pollForEpicLinkUpdatesInterval, replica, issue, connection) }) } BasicHubIssue sendEpicFirst(Long pollForEpicLinkUpdatesInterval) { send(pollForEpicLinkUpdatesInterval, { _, replica, issue, connection -> sendEpicHavingEpicFirst(pollForEpicLinkUpdatesInterval, replica, issue, connection) }) } BasicHubIssue send(Long pollForEpicLinkUpdatesInterval, Closure sendEpicFn) { def context = null if (com.exalate.processor.jira.JiraCreateReplicaProcessor.dataFilterContext != null && com.exalate.processor.jira.JiraCreateReplicaProcessor.dataFilterContext.get() == true) { context = com.exalate.processor.jira.JiraCreateReplicaProcessor.threadLocalContext.get() } else { throw new com.exalate.api.exception.IssueTrackerException(""" No context for executing external script Epic.groovy. Please contact Exalate Support.""".toString()) } final def replica = context.replica final def issue = context.issue final IConnection connection = context.connection replica.id = issue.id sendEpicFn(pollForEpicLinkUpdatesInterval, replica, issue, connection) } BasicHubIssue receive() { def context = null def remoteReplica = null if (com.exalate.processor.jira.JiraCreateIssueProcessor.createProcessorContext.get() == true) { context = com.exalate.processor.jira.JiraCreateIssueProcessor.threadLocalContext.get() remoteReplica = context.remoteReplica } else if (com.exalate.processor.jira.JiraChangeIssueProcessor.changeProcessorContext.get() == true) { context = com.exalate.processor.jira.JiraChangeIssueProcessor.threadLocalContext.get() remoteReplica = context.currentReplica } else { throw new com.exalate.api.exception.IssueTrackerException(""" No context for executing external script Epic.groovy. Please contact Exalate Support.""".toString()) } final def replica = context.replica final def issue = context.issue final def nodeHelper = context.nodeHelper final def connection = context.connection final def syncRequest = context.syncRequest final def issueBeforeScript = context.issueBeforeScript final def traces = context.traces final def blobMetadataList = context.blobMetadataList receiveEpicName(replica, issue, nodeHelper) receiveEpicLinkBeforeCreate(replica, nodeHelper, issue) if(issue.id != null){ receiveEpicLinks(replica, issue, nodeHelper) return issue } CreateIssue.create( replica, issue, syncRequest, nodeHelper, issueBeforeScript, remoteReplica, traces, blobMetadataList) { receiveEpicLinks(replica, issue, nodeHelper) return null } issue } private def cfm = com.atlassian.jira.component.ComponentAccessor.getCustomFieldManager() private def im = com.atlassian.jira.component.ComponentAccessor.getIssueManager() private def om = com.atlassian.jira.component.ComponentAccessor.getOptionsManager() private def pm = com.atlassian.jira.component.ComponentAccessor.projectManager private def itm = com.atlassian.jira.component.ComponentAccessor.getComponent(com.atlassian.jira.config.IssueTypeManager.class) private def pluginAccessor = com.atlassian.jira.component.ComponentAccessor.getPluginAccessor() private def nserv = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.node.INodeService.class) private def log = org.slf4j.LoggerFactory.getLogger("com.exalate.scripts.Epic") private def proxyAppUser = nserv.proxyUser def getEpicLinkCf = { cfm .getCustomFieldObjects()//.getCustomFieldObjectsByName("Epic Link") .find { cf -> cf.customFieldType.key == "com.pyxis.greenhopper.jira:gh-epic-link" } } def getEpicNameCf = { cfm .getCustomFieldObjects()//.getCustomFieldObjectsByName("Epic Name") .find { cf -> cf.customFieldType.key == "com.pyxis.greenhopper.jira:gh-epic-label" } } def getEpicColourCf = { cfm .getCustomFieldObjects()//.getCustomFieldObjectsByName("Epic Colour") .find { cf -> cf.customFieldType.key == "com.pyxis.greenhopper.jira:gh-epic-color" } } def getEpicStatusCf = { cfm .getCustomFieldObjects()//.getCustomFieldObjectsByName("Epic Status") .find { cf -> cf.customFieldType.key == "com.pyxis.greenhopper.jira:gh-epic-status" } } def relationLevelError2 = { String msg, Throwable cause -> new IllegalStateException(msg, cause) } def relationLevelError = { String msg -> new IllegalStateException(msg) } def toRestIssueKey = { exIssueKey -> [ "id" : exIssueKey?.id, "key": exIssueKey?.getURN(), ] as Map; } def toEpicContext = { epicIssueKey, storyIssueKeys -> [ "epic" : toRestIssueKey(epicIssueKey), "stories": storyIssueKeys ] } private List> getStories(com.exalate.basic.domain.BasicIssueKey epicExIssueKey) { def epicLinkType = ({ def esClass = null try { esClass = pluginAccessor.getClassLoader().findClass("com.atlassian.greenhopper.api.issuelink.ManagedIssueLinkTypesService") } catch (Exception e) { throw relationLevelError2("Unable to load the epic link manager class, even though the Jira Agile plugin is found. The script is wrong and should be adapted. Please contact Exalate Support.", e) } def es = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(esClass) if (es == null) { throw relationLevelError("Unable to load the epic link manager, even though the Jira Agile plugin is found. The script is wrong and should be adapted. Please contact Exalate Support.") } def iltResult = es.getEpicLinkIssueLinkType() if (!iltResult.isValid()) { def errorCollection = iltResult.errorCollection throw relationLevelError( "Unable to get the story-epic issue link: " + "\n\tErrors: " + errorCollection?.errors + "\n\tReasons: " + errorCollection?.reasons ) } def epicLinkType = iltResult.get() if (epicLinkType == null) { throw relationLevelError("Epic Link Type is null. Something is terribly wrong with either your Jira Agile installation or your script. Please contact Exalate Support.") } epicLinkType })() def epicLinkId = epicLinkType.id def ils = com.atlassian.jira.component.ComponentAccessor.getIssueLinkManager().getOutwardLinks(epicExIssueKey.id) def stories = ils .findAll { epicLinkId.equals(it.linkTypeId) } .collect { it.destinationObject } return stories.collect { story -> [ "id" : story.id, "key": story.key ] } } private BasicHubIssue sendEpicInAnyOrder(Long pollForEpicLinkUpdatesInterval, BasicHubIssue replica, BasicHubIssue issue, IConnection connection) { try{ if (pollForEpicLinkUpdatesInterval != null) { FixedIntervalSyncSchedulerEventListener.schedule(pollForEpicLinkUpdatesInterval, connection.getID(), connection.name) } else { FixedIntervalSyncSchedulerEventListener.unschedule(connection.getID(), connection.name) } }catch(ignore){ //No such property: listenerInvokers for class: com.atlassian.event.internal.LockFreeEventPublisher } def issueKey = new com.exalate.basic.domain.BasicIssueKey(issue.id as Long, issue.key) def epicLinkCf = getEpicLinkCf() def epicNameCf = getEpicNameCf() def epicColourCf = getEpicColourCf() def epicStatusCf = getEpicStatusCf() if (epicLinkCf != null && issue.customFields[epicLinkCf.idAsLong as String].value != null) { def epicLink = issue.customFields[epicLinkCf.idAsLong as String].value replica.customKeys."Epic Link" = ["id": epicLink.id, "key": epicLink.key] def exEpicIssueKey = new com.exalate.basic.domain.BasicIssueKey(epicLink.id as Long, epicLink.key as String) def stories = getStories(exEpicIssueKey) replica.customKeys."epicContext" = toEpicContext(exEpicIssueKey, stories) } if (epicNameCf != null && issue.customFields[epicNameCf.idAsLong as String].value != null) { def stories = getStories(issueKey) replica.customKeys."Epic Name" = issue.customFields[epicNameCf.idAsLong as String].value replica.customKeys."epicContext" = toEpicContext(issueKey, stories) } if (epicColourCf != null && issue.customFields[epicColourCf.idAsLong as String].value != null) { replica.customKeys."Epic Colour" = issue.customFields[epicColourCf.idAsLong as String].value } if (epicStatusCf != null && issue.customFields[epicStatusCf.idAsLong as String].value != null) { replica.customKeys."Epic Status" = issue.customFields[epicStatusCf.idAsLong as String].value.value } replica } private BasicHubIssue sendEpicHavingEpicFirst(Long pollForEpicLinkUpdatesInterval, BasicHubIssue replica, BasicHubIssue issue, IConnection connection) { try{ if (pollForEpicLinkUpdatesInterval != null) { FixedIntervalSyncSchedulerEventListener.schedule(pollForEpicLinkUpdatesInterval, connection.getID(), connection.name) } else { FixedIntervalSyncSchedulerEventListener.unschedule(connection.getID(), connection.name) } } catch(e){ //No such property: listenerInvokers for class: com.atlassian.event.internal.LockFreeEventPublisher } def issueKey = new com.exalate.basic.domain.BasicIssueKey(issue.id as Long, issue.key) def epicLinkCf = getEpicLinkCf() def epicNameCf = getEpicNameCf() def epicColourCf = getEpicColourCf() def epicStatusCf = getEpicStatusCf() if (epicLinkCf != null && issue.customFields[epicLinkCf.idAsLong as String].value != null) { def epicLink = issue.customFields[epicLinkCf.idAsLong as String].value replica.customKeys."Epic Link" = ["id": epicLink.id, "key": epicLink.key] } if (epicNameCf != null && issue.customFields[epicNameCf.idAsLong as String].value != null) { def stories = getStories(issueKey) replica.customKeys."Epic Name" = issue.customFields[epicNameCf.idAsLong as String].value replica.customKeys."epicContext" = toEpicContext(issueKey, stories) } if (epicColourCf != null && issue.customFields[epicColourCf.idAsLong as String].value != null) { replica.customKeys."Epic Colour" = issue.customFields[epicColourCf.idAsLong as String].value } if (epicStatusCf != null && issue.customFields[epicStatusCf.idAsLong as String].value != null) { replica.customKeys."Epic Status" = issue.customFields[epicStatusCf.idAsLong as String].value.value } replica } BasicHubIssue receiveEpicName(BasicHubIssue replica, BasicHubIssue issue, NodeHelper nodeHelper) { def epicNameCf = getEpicNameCf() def epicColourCf = getEpicColourCf() def epicStatusCf = getEpicStatusCf() if (replica.customKeys."Epic Name" != null) { issue.customFields[epicNameCf.idAsLong as String].value = replica.customKeys."Epic Name" as String issue.customFields.remove(epicNameCf.name as String) issue.customFields.remove(epicNameCf.untranslatedName as String) } if (replica.customKeys."Epic Colour" != null) { issue.customFields[epicColourCf.idAsLong as String].value = replica.customKeys."Epic Colour" as String issue.customFields.remove(epicColourCf.name as String) issue.customFields.remove(epicColourCf.untranslatedName as String) } if (replica.customKeys."Epic Status" != null) { def p = pm.getProjectObjByKey(issue.project?.key ?: issue.projectKey) def issueType = itm.getIssueTypes().find { it.name == (issue.type?.name ?: issue.typeName) } def ic = new com.atlassian.jira.issue.context.IssueContextImpl(p, issueType) def fieldConfig = epicStatusCf.getRelevantConfig(ic) def epicStatusOpts = om.getOptions(fieldConfig) def epicStatusOpt = epicStatusOpts.find { it.value == replica.customKeys."Epic Status" } issue.customFields[epicStatusCf.idAsLong as String].value = nodeHelper.nodeHubObjectConverter.getHubOption(epicStatusOpt) issue.customFields.remove(epicStatusCf.name as String) issue.customFields.remove(epicStatusCf.untranslatedName as String) } issue } private BasicHubIssue receiveEpicLinkBeforeCreate(com.exalate.basic.domain.hubobject.v1.BasicHubIssue replica, com.exalate.node.hubobject.v1_3.NodeHelper nodeHelper, com.exalate.basic.domain.hubobject.v1.BasicHubIssue issue) { if (replica.customKeys."Epic Link"?.id != null) { //replica.customKeys."Epic Link" = ["id": epicLink.id, "key": epicLink.key] def remoteEpicId = replica.customKeys."Epic Link".id as Long log.debug("Getting local issue by remote id " + remoteEpicId) def localHubEpic = ExalateApi.getLocalIssueFromRemoteId(remoteEpicId, nodeHelper) if (localHubEpic != null) { def localEpicJissue = im.getIssueObject(localHubEpic.id as Long) if (localEpicJissue != null) { log.info("SETTING EPIC for issue (being created for remote issue `${replica.key}` ${replica.id}) to `${localEpicJissue.key}` ${localEpicJissue.id}") def epicLinkCf = getEpicLinkCf() def epicLinkCfIdStr = epicLinkCf?.idAsLong as String issue.customFields[epicLinkCfIdStr]?.value = localEpicJissue } else { log.debug("Could not find Epic for issue (being created for remote issue `${replica.key}` ${replica.id}) by remote epic info: `${replica.customKeys."Epic Link".key}` ${replica.customKeys."Epic Link".id}") } } else { log.debug("Could not find Epic for issue (being created for remote issue `${replica.key}` ${replica.id}) by remote epic info: `${replica.customKeys."Epic Link".key}` ${replica.customKeys."Epic Link".id}") } } issue } BasicHubIssue receiveEpicLinks(BasicHubIssue replica, BasicHubIssue issue, NodeHelper nodeHelper) { if (replica .customKeys ."epicContext" == null) { return issue } log.debug("Received epic context: " + replica.customKeys."epicContext") def createdJiraIssue = im.getIssueObject(issue.id as Long) if (createdJiraIssue == null) { throw issueLevelError(""" It seems, that the issue has not been created durint create issue process. Please contact Exalate Support.""".toString()) } def epicLinkCf = getEpicLinkCf() LogIn.tryLogInFinallyLogOut { try { def getLocalIssueInternal = { remoteSuspectId, jiraIssue -> if (replica.id.equals((remoteSuspectId as Long) as String)) { return jiraIssue } log.debug("Getting local issue by remote id " + remoteSuspectId) def localHubIssue = ExalateApi.getLocalIssueFromRemoteId(remoteSuspectId as Long, nodeHelper) if (localHubIssue == null) { return null } return im.getIssueObject(localHubIssue.id as Long) } def getLocalEpicIssueInternal = { jiraIssue -> def epicContext = replica.customKeys."epicContext" if (epicContext == null) { return null } def remoteEpicId = epicContext.epic.id getLocalIssueInternal(remoteEpicId, jiraIssue) } // try to link all the stories to the epic: def localEpicJissue = getLocalEpicIssueInternal(createdJiraIssue) log.debug("Got local epic " + localEpicJissue) if (localEpicJissue != null) { replica .customKeys ."epicContext" .stories .collect { story -> log.debug("Trying to get local story for remote story with id " + story.id + ", the current issue is " + createdJiraIssue) def localIssue = getLocalIssueInternal(story.id, createdJiraIssue) log.debug("Got local story " + localIssue + "by remote story with id " + story.id) localIssue } .findAll { it != null } .each { localStory -> log.debug("linking the localStory `" + localStory.key + "` to epic `" + localEpicJissue.key + "` for the issue `" + createdJiraIssue.key + "` for remote issue `" + replica.key + "`") def mi = (localStory as com.atlassian.jira.issue.MutableIssue) mi.setCustomFieldValue(epicLinkCf, localEpicJissue) try{ im .updateIssue( proxyAppUser, mi, com.atlassian.jira.event.type.EventDispatchOption.ISSUE_UPDATED, true) } catch (groovy.lang.MissingMethodException ignore) { im .updateIssue( proxyAppUser.getDirectoryUser(), mi, com.atlassian.jira.event.type.EventDispatchOption.ISSUE_UPDATED, true) } } } def epicLinkCfIdStr = epicLinkCf?.idAsLong as String if (replica.customKeys."epicContext" == null && issue.customFields[epicLinkCfIdStr]?.value != null) { issue.customFields[epicLinkCfIdStr].value = null } } catch (com.exalate.api.exception.IssueTrackerException ite) { throw ite } catch (Exception e) { throw new com.exalate.api.exception.IssueTrackerException(e.message, e) } } issue } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~CREATE ISSUE EXTERNAL SCRIPT~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ static class CreateIssue { private static def log = org.slf4j.LoggerFactory.getLogger("com.exalate.scripts.Epic") private static def doCreate = { com.exalate.basic.domain.hubobject.v1.BasicHubIssue replica, com.exalate.basic.domain.hubobject.v1.BasicHubIssue issue, com.exalate.api.domain.request.ISyncRequest syncRequest, com.exalate.node.hubobject.v1_3.NodeHelper nodeHelper, com.exalate.basic.domain.hubobject.v1.BasicHubIssue issueBeforeScript, com.exalate.api.domain.INonPersistentReplica remoteReplica, List traces, List blobMetadataList, Logger log -> def firstSync = com.exalate.processor.jira.JiraCreateIssueProcessor.createProcessorContext.get() == true def issueLevelError = { String msg -> new com.exalate.api.exception.IssueTrackerException(msg) } def issueLevelError2 = { String msg, Throwable c -> new com.exalate.api.exception.IssueTrackerException(msg, c) } def toExIssueKey = { com.atlassian.jira.issue.MutableIssue i -> new com.exalate.basic.domain.BasicIssueKey(i.id, i.key) } final def authCtxInternal = com.atlassian.jira.component.ComponentAccessor.getJiraAuthenticationContext() final def imInternal = com.atlassian.jira.component.ComponentAccessor.issueManager final def umInternal = com.atlassian.jira.component.ComponentAccessor.userManager final def nservInternal = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.node.INodeService.class) final def hohfInternal2 = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.hubobject.IHubObjectHelperFactory.class) //noinspection GroovyAssignabilityCheck final def hohInternal2 = hohfInternal2.get(remoteReplica.payload.version) if (issue.id != null) { def existingIssue = imInternal.getIssueObject(issue.id as Long) if (existingIssue != null) { return [existingIssue, toExIssueKey(existingIssue)] } } def proxyAppUserInternal = nservInternal.getProxyUser() def loggedInUser = authCtxInternal.getLoggedInUser() log.debug("Logged user is " + loggedInUser) def reporterAppUser = null if (issue.reporter != null) { reporterAppUser = umInternal.getUserByKey(issue.reporter?.key) } reporterAppUser = reporterAppUser ?: proxyAppUserInternal issue.project = issue.project ?: ({ nodeHelper.getProject(issue.projectKey) })() issue.type = issue.type ?: ({ nodeHelper.getIssueType(issue.typeName) })() def jIssueInternal = null try { LogIn.logIn(reporterAppUser) if (issue.id != null) { def existingIssue = imInternal.getIssueObject(issue.id as Long) if (existingIssue != null) { issue.id = existingIssue.id issue.key = existingIssue.key return [existingIssue, toExIssueKey(existingIssue)] } } def cir = null try{ cir = hohInternal2.createNodeIssueWith(issue, hohInternal2.createHubIssueTemplate(), null, [:], blobMetadataList, syncRequest) } catch (MissingMethodException e){ cir = hohInternal2.createNodeIssueWith(issue, hohInternal2.createHubIssueTemplate(), null, [:], blobMetadataList, syncRequest.getConnection()) } def createdIssueKey = cir.getIssueKey(); jIssueInternal = imInternal.getIssueObject(createdIssueKey.id) if (issue.id != null) { def oldIssueKey = jIssueInternal.key def oldIssueId = jIssueInternal.id try { jIssueInternal.key = issue.key jIssueInternal.store() } catch (Exception e) { log.error("""Failed to sync issue key: ${e.message}. Please contact Exalate Support. Deleting issue `$oldIssueKey` ($oldIssueId)""".toString(), e) imInternal.deleteIssue(proxyAppUserInternal, jIssueInternal as com.atlassian.jira.issue.Issue, com.atlassian.jira.event.type.EventDispatchOption.ISSUE_DELETED, false) } } issue.id = jIssueInternal.id issue.key = jIssueInternal.key return [jIssueInternal, toExIssueKey(jIssueInternal)] } catch (com.exalate.api.exception.TransitionException te) { if (firstSync && te.issueKey && te.issueKey.id) { def jIssueInternal2 = imInternal.getIssueObject(te.issueKey.id) imInternal.deleteIssue(proxyAppUserInternal, jIssueInternal2 as com.atlassian.jira.issue.Issue, com.atlassian.jira.event.type.EventDispatchOption.ISSUE_DELETED, false) } throw te } catch (com.exalate.api.exception.IssueTrackerException ite) { if (firstSync && jIssueInternal != null) { imInternal.deleteIssue(proxyAppUserInternal, jIssueInternal as com.atlassian.jira.issue.Issue, com.atlassian.jira.event.type.EventDispatchOption.ISSUE_DELETED, false) } throw ite } catch (Exception e) { if (firstSync && jIssueInternal != null) { imInternal.deleteIssue(proxyAppUserInternal, jIssueInternal as com.atlassian.jira.issue.Issue, com.atlassian.jira.event.type.EventDispatchOption.ISSUE_DELETED, false) } throw issueLevelError2("""Failed to create issue: ${ e.message }. Please review the script or contact Exalate Support""".toString(), e) } finally { LogIn.logIn(loggedInUser) } } /** * @param whenIssueCreatedFn - a callback closure executed after the issue has been created * */ static com.exalate.basic.domain.BasicIssueKey create( com.exalate.basic.domain.hubobject.v1.BasicHubIssue replica, com.exalate.basic.domain.hubobject.v1.BasicHubIssue issue, com.exalate.api.domain.request.ISyncRequest syncRequest, com.exalate.node.hubobject.v1_3.NodeHelper nodeHelper, com.exalate.basic.domain.hubobject.v1.BasicHubIssue issueBeforeScript, com.exalate.api.domain.INonPersistentReplica remoteReplica, List traces, List blobMetadataList, Closure whenIssueCreatedFn) { def firstSync = com.exalate.processor.jira.JiraCreateIssueProcessor.createProcessorContext.get() == true def (_jIssue, _exIssueKey) = doCreate(replica, issue, syncRequest, nodeHelper, issueBeforeScript, remoteReplica, traces, blobMetadataList, log) com.atlassian.jira.issue.MutableIssue jIssue = _jIssue as com.atlassian.jira.issue.MutableIssue com.exalate.basic.domain.BasicIssueKey exIssueKey = _exIssueKey as com.exalate.basic.domain.BasicIssueKey try { whenIssueCreatedFn() UpdateIssue.update(replica, issue, syncRequest, nodeHelper, issueBeforeScript, traces, blobMetadataList, jIssue, exIssueKey) } catch (Exception e3) { final def imInternal = com.atlassian.jira.component.ComponentAccessor.issueManager final def nservInternal2 = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.node.INodeService.class) def proxyAppUserInternal = nservInternal2.getProxyUser() if (firstSync && _jIssue != null) { imInternal.deleteIssue(proxyAppUserInternal, _jIssue as com.atlassian.jira.issue.Issue, com.atlassian.jira.event.type.EventDispatchOption.ISSUE_DELETED, false) } throw e3 } return exIssueKey } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~UPDATE ISSUE EXTERNAL SCRIPT~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ static class UpdateIssue { private static def log = org.slf4j.LoggerFactory.getLogger("com.exalate.scripts.Epic") private static def doUpdate = { com.exalate.basic.domain.hubobject.v1.BasicHubIssue replica, com.exalate.basic.domain.hubobject.v1.BasicHubIssue issue, com.exalate.api.domain.request.ISyncRequest syncRequest, com.exalate.node.hubobject.v1_3.NodeHelper nodeHelper, com.exalate.basic.domain.hubobject.v1.BasicHubIssue issueBeforeScript, List traces, List blobMetadataList, com.atlassian.jira.issue.MutableIssue jIssue, com.exalate.basic.domain.BasicIssueKey exIssueKey -> try { final def hohfInternal2 = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.hubobject.IHubObjectHelperFactory.class) //noinspection GroovyAssignabilityCheck final def hohInternal2 = hohfInternal2.get("1.2.0") final def nservInternal2 = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.node.INodeService.class) def proxyAppUserInternal2 = nservInternal2.getProxyUser() log.info("performing the update for the issue `" + jIssue.key + "` for remote issue `" + replica.key + "`") //finally create all def fakeTraces2 = com.exalate.util.TraceUtils.indexFakeTraces(traces) def preparedIssue2 = hohInternal2.prepareLocalHubIssueForApplication(issueBeforeScript, issue, fakeTraces2) //@Nonnull IIssueKey issueKey, @Nonnull IHubIssueReplica hubIssueAfterScripts, @Nullable String proxyUser, @Nonnull IHubIssueReplica hubIssueBeforeScripts, @Nonnull Map> traces, @Nonnull List blobMetadataList, IRelation relation def resultTraces2 = null try{ resultTraces2 = hohInternal2.updateNodeIssueWith(exIssueKey, preparedIssue2, proxyAppUserInternal2.key, issueBeforeScript, fakeTraces2, blobMetadataList, syncRequest) } catch (MissingMethodException e){ resultTraces2 = hohInternal2.updateNodeIssueWith(exIssueKey, preparedIssue2, proxyAppUserInternal2.key, issueBeforeScript, fakeTraces2, blobMetadataList, syncRequest.getConnection()) } traces.clear() traces.addAll(resultTraces2 ?: []) new Result(issue, traces) } catch (com.exalate.api.exception.IssueTrackerException ite2) { throw ite2 } catch (Exception e2) { throw new com.exalate.api.exception.IssueTrackerException(e2.message, e2) } } static Result update(com.exalate.basic.domain.hubobject.v1.BasicHubIssue replica, com.exalate.basic.domain.hubobject.v1.BasicHubIssue issue, com.exalate.api.domain.request.ISyncRequest syncRequest, com.exalate.node.hubobject.v1_3.NodeHelper nodeHelper, com.exalate.basic.domain.hubobject.v1.BasicHubIssue issueBeforeScript, List traces, List blobMetadataList, com.atlassian.jira.issue.MutableIssue jIssue, com.exalate.basic.domain.BasicIssueKey exIssueKey) { doUpdate(replica, issue, syncRequest, nodeHelper, issueBeforeScript, traces, blobMetadataList, jIssue, exIssueKey) } static class Result { com.exalate.basic.domain.hubobject.v1.BasicHubIssue issue List traces Result(com.exalate.basic.domain.hubobject.v1.BasicHubIssue issue, java.util.List traces) { this.issue = issue this.traces = traces } } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~LOG IN EXTERNAL SCRIPT~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ private static class LogIn { static logIn(u) { def authCtx = com.atlassian.jira.component.ComponentAccessor.getJiraAuthenticationContext() try { //Jira 7 authCtx.setLoggedInUser(u) } catch (Exception ignore) { // Jira 6 //noinspection GroovyAssignabilityCheck authCtx.setLoggedInUser(u.getDirectoryUser()) } } static R tryLogInFinallyLogOut(Closure fn) { def authCtx = com.atlassian.jira.component.ComponentAccessor.getJiraAuthenticationContext() def proxyAppUser = getProxyUser() def loggedInUser = authCtx.getLoggedInUser() try { logIn(proxyAppUser) fn() } finally { logIn(loggedInUser) } } static getProxyUser() { def nserv = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.node.INodeService.class) nserv.proxyUser } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~EXALATE API EXTERNAL SCRIPT~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ static class ExalateApi { static log = org.slf4j.LoggerFactory.getLogger("com.exalate.scripts.Epic") static IConnection getConnection(int id) { try { // Exalate 4.2.X def relrepo = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.persistence.relation.IRelationRepository.class) relrepo.getRelation(id) } catch (MissingMethodException ignore) { // Exalate 4.3.X def relrepo = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.persistence.relation.IRelationRepository.class) relrepo.getConnectionById(id) } } static IConnection getConnection(Integer id) { if (id == null) { return null } getConnection(id.intValue()) } static BasicHubIssue getLocalIssueFromRemoteId(remoteIssueId, NodeHelper nodeHelper) { if (remoteIssueId == null) { return null } def im = com.atlassian.jira.component.ComponentAccessor.getIssueManager() def tt = nodeHelper.twinTraceRepository.getTwinTraceByRemoteIssueId(nodeHelper.connection, (remoteIssueId as String)) log.debug("Got twintrace by remote issue id " + remoteIssueId + "twintrace is " + tt) def _jIssue = tt != null && tt.localReplica != null ? im.getIssueObject(tt.localReplica.issueKey.id as Long) : null log.debug("Got issue " + _jIssue + " from twintrace " + tt) if (_jIssue == null) { return null } def localIssue = nodeHelper.nodeHubObjectConverter.getHubIssue(_jIssue) log.debug("Got basic hub issue " + localIssue + " from issue " + _jIssue) return localIssue } static boolean isUnderSync(String localIssueIdStr) { if (localIssueIdStr == null) { return null } def ttRepo = ComponentAccessor.getOSGiComponentInstanceOfType(ITwinTraceRepository.class) def ttSeqFuture = ttRepo.findTwinTracesByIssueId(localIssueIdStr) def ttSeq = FutureUtils.resultInf(ttSeqFuture) as Seq; !ttSeq.isEmpty() } static com.atlassian.jira.user.ApplicationUser getProxyUser() { def nserv = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.node.INodeService.class) nserv.proxyUser } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~FIXED INTERVAL SYNC SCHEDULER EVENT LISTENER EXTERNAL SCRIPT~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ static class FixedIntervalSyncSchedulerEventListener { static LOG = org.slf4j.LoggerFactory.getLogger("com.exalate.scripts.FixedIntervalSyncSchedulerEventListener") static String getKey(Integer connectionId) { "com.exalate.scripts.FixedIntervalSyncSchedulerEventListener.connections.$connectionId".toString() } private static com.atlassian.event.api.EventPublisher getEp() { com.atlassian.jira.component.ComponentAccessor.getComponent(com.atlassian.event.api.EventPublisher.class) } static schedule(Long interval, Integer connectionId, String connectionName) { def ep = getEp() def entries = ep.listenerInvokers.entries() def listenerKey = getKey(connectionId) def fixedIntervalSyncSchedulerEntries = entries.findAll { listenerInvokerEntry -> def listenerInvoker = listenerInvokerEntry.value listenerInvoker.key == listenerKey } //as Collection, com.atlassian.event.internal.EventPublisherImpl.KeyedListenerInvoker>> def fixedIntervalSyncScheduler = fixedIntervalSyncSchedulerEntries.find()?.value?.invoker?.listener // as FixedIntervalSyncSchedulerEventListener if (fixedIntervalSyncScheduler == null) { if (interval != null) { ep.registerListener(listenerKey, new FixedIntervalSyncSchedulerEventListener(interval, connectionId, connectionName)) } else { LOG.debug("The interval, specified for FixedIntervalSyncSchedulerEventListener.schedule is null, so even though there's no listener yet, we're not hanging a new one either.".toString()) } } else if (fixedIntervalSyncScheduler.interval != interval) { LOG.debug("Found an existing FixedIntervalSyncSchedulerEventListener, the interval seems to be different: fixedIntervalSyncScheduler.interval=`${fixedIntervalSyncScheduler.interval}` interval=`$interval` unregistering it.".toString()) fixedIntervalSyncScheduler.unregister() if (interval != null) { ep.registerListener(listenerKey, new FixedIntervalSyncSchedulerEventListener(interval, connectionId, connectionName)) } else { LOG.debug("The interval, specified for FixedIntervalSyncSchedulerEventListener.schedule is null, so even though has been a listener, we're not hanging a new one either.".toString()) } } } static unschedule(Integer connectionId, String connectionName) { schedule(null, connectionId, connectionName) } class FixedIntervalSyncSchedulerWorker implements Runnable { void run() { try { LOG.trace("FixedIntervalSyncSchedulerWorker - ${new Date().format("HH:mm:ss.SSS")}: trying to find some sync to do".toString()) def ttrepo = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.persistence.twintrace.ITwinTraceRepository.class) def ess = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.replication.out.IEventSchedulerService.class) def con = getConnection() if (con) { def tts = ttrepo.getTwinTracesForRelation(con) tts.each { tt -> def exIssueKey = tt?.localReplica?.issueKey if (exIssueKey != null && exIssueKey.id != null && exIssueKey.URN != null) { def iis = com.atlassian.jira.component.ComponentAccessor.getComponent(com.atlassian.jira.issue.index.IssueIndexingService.class) def im = com.atlassian.jira.component.ComponentAccessor.getIssueManager() def jIssue = im.getIssueObject(exIssueKey.id as Long) if (jIssue) { iis.reIndex(jIssue) } ess.scheduleSyncEvent(exIssueKey, tt) } else { LOG.trace("It seems, issue `${exIssueKey?.URN}` (${exIssueKey?.id}) is not under sync yet. Not scheduling an update for it".toString()) } } } else { LOG.debug("The connection `${connectionName}` ($connectionId) seems to be gone now. Stopping the FixedIntervalSyncSchedulerEventListener!".toString()) unregister() } } catch (Exception e){ //safety precaution if this listener has bugs, we unregister it //but we also log the problems into the log file LOG.error("""The FixedIntervalSyncSchedulerWorker from FixedIntervalSyncSchedulerEventListener.groovy had a problem: `${e.message}`. Please contact Exalate support team, changes to epic links and issue links would only be synchronized if other changes to these issues are performed.""".toString(), e) unregister() } } } final Long interval final Integer connectionId final String connectionName final String listenerKey final java.util.concurrent.ScheduledExecutorService scheduledExecutor final java.util.concurrent.ScheduledFuture scheduledFuture FixedIntervalSyncSchedulerEventListener(Long interval, Integer connectionId, String connectionName) { this.connectionId = connectionId this.connectionName = connectionName this.listenerKey = getKey(connectionId) this.interval = interval scheduledExecutor = java.util.concurrent.Executors.newSingleThreadScheduledExecutor() scheduledFuture = scheduledExecutor.scheduleWithFixedDelay( new FixedIntervalSyncSchedulerWorker(), 0L, interval, java.util.concurrent.TimeUnit.SECONDS ) } @com.atlassian.event.api.EventListener void onJiraEvent (Object jiraEventObj) { try { //safety precaution - unregister this listener if Exalate plugin is disabled if (jiraEventObj instanceof com.atlassian.plugin.event.events.PluginDisabledEvent) { def pde = jiraEventObj as com.atlassian.plugin.event.events.PluginDisabledEvent def plugin = pde.plugin if (plugin.key == "com.exalate.jiranode") { LOG.info(""" The Exalate app is being disabled. Unregistering the FixedIntervalSyncSchedulerEventListener ($listenerKey). """.toString()) unregister() return } } def _PluginDisablingEventClazz = null try { _PluginDisablingEventClazz = com.atlassian.jira.component.ComponentAccessor.classLoader.loadClass("com.atlassian.plugin.event.events.PluginDisablingEvent") } catch (Exception ignore) { // ... } if (_PluginDisablingEventClazz && _PluginDisablingEventClazz.isAssignableFrom(jiraEventObj.getClass())) { def pde = jiraEventObj def plugin = pde.plugin if (plugin.key == "com.exalate.jiranode") { LOG.info(""" The Exalate app has been disabled. Unregistering the ExalateSprintEventListener. """.toString()) unregister() return } } } catch (Exception e) { //safety precaution if this listener has bugs, we unregister it //but we also log the problems into the log file LOG.error("""The FixedIntervalSyncSchedulerEventListener from AgileSync.groovy had a problem: `${e.message}` Please contact Exalate support team, changes to issue links and epic links would only be synchronized if changes to the issues are performed after linking.""".toString(), e) try { unregister() } catch (Exception ignore) { } } } void unregister(){ scheduledFuture.cancel(false) scheduledExecutor.shutdown() def ep = getEp() ep.unregisterListener(listenerKey) } private com.exalate.api.domain.connection.IConnection getConnection() { ExalateApi.getConnection(connectionId) } } }