import com.atlassian.event.api.EventPublisher import com.exalate.api.domain.connection.IConnection import com.exalate.basic.domain.hubobject.v1.BasicHubIssue import com.exalate.node.hubobject.v1_3.NodeHelper import org.slf4j.Logger /** Usage: Add the snippet below to the end of your "Outgoing sync": SimpleSprintSync.send() // listenToSprintEvents = false -------------------------------- Add the snippet below to the end of your "Incoming sync": SimpleSprintSync.receive() -------------------------------- * */ class SimpleSprintSync { static BasicHubIssue send(boolean listenToSprintEvents = false) { return (new SprintCtx()).sendSprints(listenToSprintEvents) } static BasicHubIssue receive(Map projectToSprintPrefixNameMapping = [:]) { return (new SprintCtx()).receiveSprints(projectToSprintPrefixNameMapping) } static def issueLevelError(String msg) { new com.exalate.api.exception.IssueTrackerException(msg) } static class SprintCtx { private def cfm = com.atlassian.jira.component.ComponentAccessor.getCustomFieldManager() private def im = com.atlassian.jira.component.ComponentAccessor.getIssueManager() 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 sserv = com.atlassian.jira.component.ComponentAccessor.getComponent(com.atlassian.jira.bc.issue.search.SearchService.class) private def log = org.slf4j.LoggerFactory.getLogger("com.exalate.scripts.Sprint") private def proxyAppUser = nserv.proxyUser //com.atlassian.jira.user.ApplicationUser u def parseQuery = { u, String qStr -> try { // Jira 7 way return sserv.parseQuery(u, qStr) } catch (MissingMethodException ignore) { // Jira 6 way return sserv.parseQuery(u.getDirectoryUser(), qStr) } } //com.atlassian.jira.user.ApplicationUser u def searchOverrideSecurity = { u, finalQuery -> try { try { // Jira 8 way def _ThreadLocalSearcherCache = com.atlassian.jira.component.ComponentAccessor.classLoader.loadClass("com.atlassian.jira.issue.index.ThreadLocalSearcherCache") _ThreadLocalSearcherCache.startSearcherContext() def res = sserv.searchOverrideSecurity(u, finalQuery, com.atlassian.jira.web.bean.PagerFilter.getUnlimitedFilter()) _ThreadLocalSearcherCache.stopAndCloseSearcherContext() return res } catch (Exception ignore) { // Jira 7 way return sserv.searchOverrideSecurity(u, finalQuery, com.atlassian.jira.web.bean.PagerFilter.getUnlimitedFilter()) } } catch (MissingMethodException ignore) { // Jira 6 way: // SearchResults search(User var1, Query var2, PagerFilter var3) throws SearchException; //noinspection GroovyAssignabilityCheck return sserv.search(u.getDirectoryUser(), finalQuery, com.atlassian.jira.web.bean.PagerFilter.getUnlimitedFilter()) } } BasicHubIssue sendSprints(boolean listenToSprintEvents) { 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 Sprint.groovy. Please contact Exalate Support.""".toString()) } final def replica = context.replica final def issue = context.issue final IConnection connection = context.connection def issueLevelError = { String msg -> new com.exalate.api.exception.IssueTrackerException(msg) } def sprintCf = com.atlassian.jira.component.ComponentAccessor.getCustomFieldManager() .getCustomFieldObjects()//.getCustomFieldObjectsByName("Sprint") .find { cf -> cf.customFieldType.key == "com.pyxis.greenhopper.jira:gh-sprint" } def jiraCl = com.atlassian.jira.component.ComponentAccessor.classLoader def iSO8601DateFormatClass = null def iso8601DateFormat = null try { iSO8601DateFormatClass = jiraCl.loadClass("org.apache.log4j.helpers.ISO8601DateFormat") iso8601DateFormat = iSO8601DateFormatClass.newInstance() } catch (Exception ignore) { //... } def getSprintJson = { sprint -> def startDate = ({ try { sprint.startDate?.millis as Long } catch (Exception ignore) { null } })() def endDate = ({ try { sprint.endDate?.millis as Long } catch (Exception ignore) { null } })() def completeDate = ({ try { sprint.completeDate?.millis as Long } catch (Exception ignore) { null } })() def sprintId = ({ try { sprint.id as Long } catch (Exception ignore) { null } })() def resultJson = [ "id" : sprintId, "state" : ({ try { (sprint.state.name() as String)?.toLowerCase() } catch (Exception ignore) { null } })(), "name" : ({ try { sprint.name as String } catch (Exception ignore) { null } })(), "startDate" : ({ try { startDate == null ? null : iso8601DateFormat.format(new Date(startDate as Long)) } catch (Exception ignore) { null } })(), "endDate" : ({ try { startDate == null ? null : iso8601DateFormat.format(new Date(endDate as Long)) } catch (Exception ignore) { null } })(), "completeDate" : ({ try { startDate == null ? null : iso8601DateFormat.format(new Date(completeDate as Long)) } catch (Exception ignore) { null } })(), "originBoardId": ({ try { sprint.rapidViewId as String } catch (Exception ignore) { null } })(), "sequence" : ({ try { sprint.sequence as String } catch (Exception ignore) { null } })(), "goal" : ({ try { sprint.goal as String } catch (Exception ignore) { null } })() ] /* { "id": 37, "self": "http://www.example.com/jira/rest/agile/1.0/sprint/23", "state": "closed", "name": "sprint 1", "startDate": "2015-04-11T15:22:00.000+10:00", "endDate": "2015-04-20T01:22:00.000+10:00", "completeDate": "2015-04-20T11:04:00.000+10:00", "originBoardId": 5, "goal": "sprint 1 goal" } */ if (!(resultJson instanceof Map)) { throw issueLevelError("Sprint " + sprintId + " json has unrecognized structure, please contact Exalate Support: " + sprint.dump()) } def resultMap = resultJson as Map if (startDate != null) { resultMap.startDateLong = startDate } if (endDate != null) { resultMap.endDateLong = endDate } if (completeDate != null) { resultMap.completeDateLong = completeDate } resultMap } def getSprintContext = { List sprints -> sprints.inject([] as List>) { List> result, sprint -> result += [ "sprint": getSprintJson(sprint), "issues": [] as List> ] result } } if(sprintCf?.idAsLong){ def sprintCfValue = issue.customFields[sprintCf?.idAsLong as String]?.value if (sprintCf != null && sprintCfValue != null) { replica.customKeys."sprintContext" = getSprintContext(sprintCfValue) } } replica.id = issue.id replica } BasicHubIssue receiveSprints(Map projectToSprintPrefixNameMapping) { 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 Sprint.groovy. Please contact Exalate Support.""".toString()) } def keepOnlySyncedIssuesInSprint = false def deleteActiveSprints = false 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 if(issue.id != null){ receiveSprints(projectToSprintPrefixNameMapping, keepOnlySyncedIssuesInSprint, deleteActiveSprints, replica, issue, nodeHelper) return issue } CreateIssue.create( replica, issue, syncRequest, nodeHelper, issueBeforeScript, remoteReplica, traces, blobMetadataList) { receiveSprints(projectToSprintPrefixNameMapping, keepOnlySyncedIssuesInSprint, deleteActiveSprints, replica, issue, nodeHelper) return null } issue } BasicHubIssue receiveSprints(Map projectToSprintPrefixNameMapping, boolean keepOnlySyncedIssuesInSprint, boolean deleteActiveSprints, BasicHubIssue replica, BasicHubIssue issue, NodeHelper nodeHelper) { def localExIssueKey = new com.exalate.basic.domain.BasicIssueKey(issue.id as Long, issue.key) def jiraCl = com.atlassian.jira.component.ComponentAccessor.classLoader def iSO8601DateFormatClass = ({ try { jiraCl.loadClass("org.apache.log4j.helpers.ISO8601DateFormat.ISO8601DateFormat") } catch (Exception ignore) { null } })() final def iso8601DateFormat = iSO8601DateFormatClass?.newInstance() final def jagPlugin = pluginAccessor.getEnabledPlugin("com.pyxis.greenhopper.jira") if(jagPlugin == null) { //no greenhoper plugin return issue } final def jagCl = jagPlugin.classLoader final def jagCa = jagPlugin.containerAccessor final def rvservClass = jagCl.loadClass("com.atlassian.greenhopper.service.rapid.view.RapidViewService") final def rvserv = jagCa.getBeansOfType(rvservClass).first() final def prvservClass = jagCl.loadClass("com.atlassian.greenhopper.service.rapid.ProjectRapidViewService") final def prvserv = jagCa.getBeansOfType(prvservClass).first() final def prsClass = jagCl.loadClass("com.atlassian.greenhopper.service.PageRequests") final def sqClass = jagCl.loadClass("com.atlassian.greenhopper.service.sprint.SprintQuery") final def sprintServClass = jagCl.loadClass("com.atlassian.greenhopper.service.sprint.SprintService") final def sprintServ = jagCa.getBeansOfType(sprintServClass).first() final def sprintIssueServClass = jagCl.loadClass("com.atlassian.greenhopper.service.sprint.SprintIssueService") final def sprintIssueServ = jagCa.getBeansOfType(sprintIssueServClass).first() def getPageReq = { Long offset, Integer limit -> prsClass.request(offset, limit) } final def emptySprintQuery = sqClass.noQuery() final def sprintClass = jagCl.loadClass("com.atlassian.greenhopper.service.sprint.Sprint") def sprintCf = cfm .getCustomFieldObjects()//.getCustomFieldObjectsByName("Sprint") .find { cf -> cf.customFieldType.key == "com.pyxis.greenhopper.jira:gh-sprint" } def issueLevelError = { String msg -> new com.exalate.api.exception.IssueTrackerException(msg) } def issueLevelError2 = { String msg, Throwable t -> new com.exalate.api.exception.IssueTrackerException(msg, t) } def _paginate _paginate = { Long offset, Integer limit, List result, Closure nextPageFn, Closure getTotalFn -> def page = nextPageFn(offset, limit) def total = getTotalFn(page) def last = total < limit //noinspection GroovyAssignabilityCheck def newResult = ([] + result) as List newResult.add(page) if (last) { newResult } else { _paginate(offset + limit, limit, newResult, nextPageFn, getTotalFn) } } def paginate = { Integer limit, Closure nextPageFn, Closure getTotalFn -> _paginate(0L, limit, [], nextPageFn, getTotalFn) } def getSprintJson = { sprint -> def startDate = ({ try { sprint.startDate?.millis as Long } catch (Exception ignore) { null } })() def endDate = ({ try { sprint.endDate?.millis as Long } catch (Exception ignore) { null } })() def completeDate = ({ try { sprint.completeDate?.millis as Long } catch (Exception ignore) { null } })() def sprintId = ({ try { sprint.id as Long } catch (Exception ignore) { null } })() def resultJson = [ "id" : sprintId, "state" : ({ try { (sprint.state.name() as String)?.toLowerCase() } catch (Exception ignore) { null } })(), "name" : ({ try { sprint.name as String } catch (Exception ignore) { null } })(), "startDate" : ({ try { startDate == null ? null : iso8601DateFormat.format(new Date(startDate as Long)) } catch (Exception ignore) { null } })(), "endDate" : ({ try { startDate == null ? null : iso8601DateFormat.format(new Date(endDate as Long)) } catch (Exception ignore) { null } })(), "completeDate" : ({ try { startDate == null ? null : iso8601DateFormat.format(new Date(completeDate as Long)) } catch (Exception ignore) { null } })(), "originBoardId": ({ try { sprint.rapidViewId as String } catch (Exception ignore) { null } })(), "sequence" : ({ try { sprint.sequence as String } catch (Exception ignore) { null } })(), "goal" : ({ try { sprint.goal as String } catch (Exception ignore) { null } })() ] /* { "id": 37, "self": "http://www.example.com/jira/rest/agile/1.0/sprint/23", "state": "closed", "name": "sprint 1", "startDate": "2015-04-11T15:22:00.000+10:00", "endDate": "2015-04-20T01:22:00.000+10:00", "completeDate": "2015-04-20T11:04:00.000+10:00", "originBoardId": 5, "goal": "sprint 1 goal" } */ if (!(resultJson instanceof Map)) { throw issueLevelError("Sprint " + sprintId + " json has unrecognized structure, please contact Exalate Support: " + sprint.dump()) } def resultMap = resultJson as Map; if (startDate != null) { resultMap.startDateLong = startDate } if (endDate != null) { resultMap.endDateLong = endDate } if (completeDate != null) { resultMap.completeDateLong = completeDate } resultMap } def getGhSprintsOnBoard = { Long boardId -> //ServiceOutcome getRapidView(@Nullable ApplicationUser var1, @Nonnull Long var2) def boardResult = rvserv.getRapidView(proxyAppUser, boardId) if (!boardResult.valid) { throw issueLevelError( """ |Can not find board by id $boardId |as user `${proxyAppUser.username}` (${proxyAppUser.key}), |errors: `${boardResult.errors.errors}` |reasons: `${boardResult.errors.reasons}` |please log in as that user and try to find that board, alternatively, contact Exalate Support |""".stripMargin().toString() ) } final def board = boardResult.get() def sprintsOnBoardPageJsons = paginate( 50, { Long offset, Integer limit -> // ServiceOutcome> getSprints(@Nullable ApplicationUser user, RapidView rapidView, PageRequest pageRequest, SprintQuery query) //JIRA 8 compatibility - using ThreadLocalSearcherCache def sprintPageResult = null try { def _ThreadLocalSearcherCache = com.atlassian.jira.component.ComponentAccessor.classLoader.loadClass("com.atlassian.jira.issue.index.ThreadLocalSearcherCache") _ThreadLocalSearcherCache.startSearcherContext() sprintPageResult = sprintServ.getSprints(proxyAppUser, board, getPageReq(offset, limit), emptySprintQuery) _ThreadLocalSearcherCache.stopAndCloseSearcherContext() } catch (Exception ignore) { // ... sprintPageResult = sprintServ.getSprints(proxyAppUser, board, getPageReq(offset, limit), emptySprintQuery) } if (!sprintPageResult.valid) { throw issueLevelError( """ |Can not find sprints on the board `${board.name as String}` ($boardId) |as user `${proxyAppUser.username}` (${proxyAppUser.key}), |errors: `${sprintPageResult.errors.errors}` |reasons: `${sprintPageResult.errors.reasons}` |please log in as that user and try to find that board, alternatively, contact Exalate Support |""".stripMargin().toString() ) } def sprintPage = sprintPageResult.get() /* Long getStart(); Integer maxResults(); @Nullable Long getTotal(); Boolean isLast(); List getValues(); */ def resultJson = [ "maxResults": sprintPage.maxResults() as Long, "startAt" : sprintPage.start as Long, "total" : sprintPage.total as Long, "isLast" : sprintPage.isLast as Boolean, "values" : (sprintPage.values as List), ] resultJson as Map }, { Map page -> (page.values as List>).size() } ) sprintsOnBoardPageJsons .collect { it.values as List } .flatten() } def getSprintsOnBoard = { Long boardId -> getGhSprintsOnBoard(boardId).collect(getSprintJson) } def createSprint = { Map remoteSprint, Long localBoardId -> def sprintToBeCreated = sprintClass.builder() .rapidViewId(localBoardId) .name(remoteSprint.name as String) .startDate(remoteSprint.startDateLong as Long) .endDate(remoteSprint.endDateLong as Long) .build() def response = null try { //ServiceOutcome createSprint(@Nullable ApplicationUser var1, @Nonnull Sprint var2); response = sprintServ.createSprint(proxyAppUser, sprintToBeCreated) // Successful creation of Sprint ProgressLog.logSprintChange("CREATE SPRINT", sprintToBeCreated.name) } catch (Exception e) { throw issueLevelError2( "Unable to create sprint `${sprintToBeCreated.dump()}` on behalf of `${proxyAppUser.username}` (${proxyAppUser.key}), please contact Exalate Support: ${e.message}".toString(), e ) } if (!response.valid) { throw issueLevelError( """ |Can not create sprint `${sprintToBeCreated.dump()}` on the board $localBoardId |as user `${proxyAppUser.username}` (${proxyAppUser.key}), |errors: `${response.errors.errors}` |reasons: `${response.errors.reasons}` |please log in as that user and try to find that board, alternatively, contact Exalate Support |""".stripMargin().toString() ) } def sprint = response.get() getSprintJson(sprint) } def getAllBoards = { String name, String type, String projectKey -> def response = null try { //ServiceOutcome> getRapidViews(@Nullable final ApplicationUser user) if (projectKey == null) { response = rvserv.getRapidViews(proxyAppUser) } else { //findRapidViewsByProject(@Nullable ApplicationUser var1, @Nonnull Project var2) def _p = com.atlassian.jira.component.ComponentAccessor.projectManager.getProjectObjByKey(projectKey) response = prvserv.findRapidViewsByProject(proxyAppUser, _p) } } catch (Exception e) { throw issueLevelError2( """ |Unable to get boards |as user `${proxyAppUser.username}` (${proxyAppUser.key}), |error: `${e.message}` |please log in as that user and try to find that sprint, alternatively, contact Exalate Support |""".stripMargin().toString() , e ) } if (!response.valid) { throw issueLevelError( """ |Can not get boards |as user `${proxyAppUser.username}` (${proxyAppUser.key}), |errors: `${response.errors.errors}` |reasons: `${response.errors.reasons}` |please log in as that user and try to find that sprint, alternatively, contact Exalate Support |""".stripMargin().toString() ) } def boards = response.get() def resultBoards = boards .collect { b -> [ "id" : b.id as Long, "name": b.name as String, "type": (b.type.name() as String)?.toLowerCase(), ] } if (type != null) { resultBoards.findAll { b -> type == b.type } } else { resultBoards } } def getSprintByRemote = { String remoteSprintName, Long remoteSprintId, String localProjectKey -> getAllBoards(null, "scrum", localProjectKey).inject(null as Map) { result, board -> if (result != null) result else getSprintsOnBoard(board.id as Long).find { Map s -> // TODO: use id mapping instead s.name == remoteSprintName } } } def getJiraSprint = { Long sprintId -> def result = sprintServ.getSprint(proxyAppUser, sprintId) if (!result.valid) { throw issueLevelError( """ |Can not get sprint $sprintId |as user `${proxyAppUser.username}` (${proxyAppUser.key}), |errors: `${result.errors.errors}` |reasons: `${result.errors.reasons}` |please log in as that user and try to find that sprint, alternatively, contact Exalate Support |""".stripMargin().toString() ) } result.get() } def moveToSprint = { Long sprintId, List issues -> // final Map json = [ // "issues": issues.collect { i -> i.id } // ] // final def jsonStr = JsonOutput.toJson(json) def sprint = getJiraSprint(sprintId) def jIssues = issues.collect { anIssueKey -> im.getIssueObject(anIssueKey.id as Long) } def response = null try { // log.debug("calling the sprintIssueServ.moveIssuesToSprint(`${proxyAppUser.key}`, `${sprint?.id}`, `${jIssues?.key?.join(", ")}`)") //ServiceResult moveIssuesToSprint(@Nullable ApplicationUser user, Sprint sprint, Collection issues) response = sprintIssueServ.moveIssuesToSprint(proxyAppUser, sprint, jIssues) } catch (Exception e) { throw issueLevelError2( """ |Unable to move issues `${issues.collect { it.URN }.join(", ")}` to sprint `${sprint.name}` (${sprintId}) |as user `${proxyAppUser.username}` (${proxyAppUser.key}), |error: `${e.message}` |please log in as that user and try to find that sprint, alternatively, contact Exalate Support |""".stripMargin().toString() , e ) } if (!response.isValid()) { throw issueLevelError( """ |Can not move issues `${issues.collect { it.URN }.join(", ")}` to sprint `${sprint.name}` (${sprintId}) |as user `${proxyAppUser.username}` (${proxyAppUser.key}), |errors: `${response.errors.errors}` |reasons: `${response.errors.reasons}` |please log in as that user and try to find that sprint, alternatively, contact Exalate Support |""".stripMargin().toString() ) } } def moveToSprintMultiple = { Long sprintId, List issues -> if (issues.empty) { return } // log.debug("Moving issues " + issues + " to sprint " + sprintId) moveToSprint(sprintId, issues) } def moveToBacklog = { List issues -> def jIssues = issues.collect { anIssueKey -> im.getIssueObject(anIssueKey.id as Long) } def response = null try { //ServiceResult moveIssuesToBacklog(@Nullable ApplicationUser user, Collection issues) response = sprintIssueServ.moveIssuesToBacklog(proxyAppUser, jIssues) } catch (Exception e) { throw issueLevelError2( """ |Unable to move issues `${issues.collect { it.URN }.join(", ")}` to backlog |as user `${proxyAppUser.username}` (${proxyAppUser.key}), |error: `${e.message}` |please log in as that user and try to find that sprint, alternatively, contact Exalate Support |""".stripMargin().toString() , e ) } if (!response.isValid()) { throw issueLevelError( """ |Can not move issues `${issues.collect { it.URN }.join(", ")}` to backlog |as user `${proxyAppUser.username}` (${proxyAppUser.key}), |errors: `${response.errors.errors}` |reasons: `${response.errors.reasons}` |please log in as that user and try to find that sprint, alternatively, contact Exalate Support |""".stripMargin().toString() ) } } def moveToBacklogMultiple = { List issues -> if (issues.empty) { return } moveToBacklog(issues) } def toRestIssueKey = { jiraIssue -> [ "id" : jiraIssue?.id, "key": jiraIssue?.key, ] } def search = { String jql -> //get board query def queryRes = parseQuery(proxyAppUser, jql) if (!queryRes.valid) { throw issueLevelError("Failed to parse JQL `$jql`: `${queryRes.errors.errorMessages}`. Please review the script.".toString()) } def query = queryRes.query //add ranking order by // find issues on board def sRes = searchOverrideSecurity(proxyAppUser, query) def issues = null try { // JIRA 6, 7 issues = sRes.issues } catch(Exception ignore) { // JIRA 8 issues = sRes.results } def issuekeys = issues.collect(toRestIssueKey) issuekeys } try { // apply sprints /* 1. iterate over sprints in context: 1.0. check whether sprint by name is present if local is closed, ignore if local is active, put current issue into it if local is future, put current issue into it if local is none, create, put current issue into it */ def remoteSprintContext = replica.customKeys.sprintContext ?: [] // // re-order the sprints in such a way that CLOSED sprints come first // (remoteSprintContext as List>).sort { Map ctx -> // def remoteSprint = ctx.sprint as Map; // if ("CLOSED".equalsIgnoreCase(remoteSprint.state as String)) -1 // else 1 // } // allowing local sprint sync by mapping the names of sprints remoteSprintContext.each {Map ctx -> def remoteSprint = ctx.sprint as Map; if (!projectToSprintPrefixNameMapping.isEmpty()) { def projectKey = issue.projectKey ?: issue.project?.key if (projectKey != null) { def sprintPrefixFromMap = projectToSprintPrefixNameMapping.get(projectKey as String) if(sprintPrefixFromMap != null && !sprintPrefixFromMap.isEmpty) { //We do not use id of sprint right now def remoteSprintName = remoteSprint.name remoteSprint.put("name", sprintPrefixFromMap + remoteSprintName) } } }} //Get all sprints in the issue history def localSprints = (issue.customFields[sprintCf.idAsLong as String]?.value ?: []).collect(getSprintJson) def localProjectKey = (issue.projectKey ?: issue.project?.key) as String def localBoardId = getAllBoards(null, "scrum", localProjectKey)?.find()?.id as Long if (localBoardId == null) { throw new com.exalate.api.exception.IssueTrackerException("Failing to synchronize the sprints for `${issue.key}` because there seems to be no scrum board for project `${localProjectKey}`. Please create one or disable sprint sync for issues in this project.") } replica.customKeys.sprintContext?.each { Map ctx -> def remoteSprint = ctx.sprint as Map; def localSprint = getSprintByRemote(remoteSprint.name as String, remoteSprint.id as Long, localProjectKey) log.debug("Got localSprint: `" + localSprint + "` for issue `${issue.key}`") def localSprintId = localSprint?.id as Long if (localSprint == null && !"CLOSED".equalsIgnoreCase(remoteSprint.state as String)) { localSprint = createSprint(remoteSprint, localBoardId) localSprintId = localSprint?.id as Long } def alreadyHasSprint = localSprints.any { sprint -> (sprint.id as Long) == localSprintId } // log.debug("Checking if we can move the issue to the sprint: `" + localSprintId + "` alreadyHasSprint=`${alreadyHasSprint}` for issue `${issue.key}`") if (localSprintId != null && !"CLOSED".equalsIgnoreCase(localSprint.state as String) && !alreadyHasSprint) { moveToSprintMultiple(localSprintId, [localExIssueKey]) // log.debug("Issues in sprint `${localSprintId}` after move : ${search("sprint = ${localSprintId}")}") } } // if there is no remote future or active sprint, move the issue to backlog if (!replica.customKeys.sprintContext?.any { Map ctx -> def remoteSprint = ctx.sprint as Map; !"CLOSED".equalsIgnoreCase(remoteSprint.state as String) }) { moveToBacklogMultiple([localExIssueKey]) } issue.customFields.remove(sprintCf.name) issue.customFields.remove(sprintCf.idAsLong as String) } 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.SimpleSprintSync") 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.SimpleSprintSync") 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()) } } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~PROGRESS LOG EXTERNAL SCRIPT~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ private static class ProgressLog { private static progresslog = org.slf4j.LoggerFactory.getLogger("com.exalate.scripts.Sprint") static void logSprintChange(String event, String sprint) { progresslog.trace("${event}: '$sprint'") } } }