import com.atlassian.jira.component.ComponentAccessor import com.exalate.api.domain.connection.IConnection import com.exalate.api.domain.twintrace.ITwinTrace import com.exalate.api.exception.IssueTrackerException 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 groovy.json.JsonOutput import groovy.json.JsonSlurper import scala.collection.immutable.Seq /** Usage: Add the snippet below to the end of your "Outgoing sync": Components.send() // listenToComponentEvents = false -------------------------------- Add the snippet below to the end of your "Incoming sync": Components.receive() -------------------------------- * */ class Components { static log = org.slf4j.LoggerFactory.getLogger("com.exalate.scripts.Components") static getContext(){ 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 if (com.exalate.processor.jira.JiraCreateIssueProcessor.createProcessorContext != null && com.exalate.processor.jira.JiraCreateIssueProcessor.createProcessorContext.get() == true) { context = com.exalate.processor.jira.JiraCreateIssueProcessor.threadLocalContext.get() } else if (com.exalate.processor.jira.JiraChangeIssueProcessor.changeProcessorContext != null && com.exalate.processor.jira.JiraChangeIssueProcessor.changeProcessorContext.get() == true) { context = com.exalate.processor.jira.JiraChangeIssueProcessor.threadLocalContext.get() } else { throw new com.exalate.api.exception.IssueTrackerException(""" No context for executing external script Components.groovy. Please contact Exalate Support.""".toString()) } return context } static getStorage(IConnection connection) { MappingStorageSupplier.get("com.exalate.scripts.ComponentSync.v0.connections.${connection.getID()}".toString()) } static BasicHubIssue send(boolean listenToComponentEvents = false){ def context = getContext() final def replica = context.replica final def issue = context.issue final def connection = context.connection try{ if (listenToComponentEvents) { def connectionId = connection.getID() def connectionName = connection.name ExalateComponentEventListener.listen(connectionId, connectionName) } else { ExalateComponentEventListener.unlisten(connection.getID()) } } catch(ignore){ //No such property: listenerInvokers for class: com.atlassian.event.internal.LockFreeEventPublisher } replica.components = issue.components return replica } static BasicHubIssue receive() { def context = getContext() final def replica = context.replica final def issue = context.issue final def connection = context.connection final def nodeHelper = context.nodeHelper issue.components = receive(replica.components, issue.project?.key ?: issue.projectKey, connection, nodeHelper) issue } static List receive(List remoteComponents, String localProjectKey, IConnection connection, NodeHelper nodeHelper) { receive(true, remoteComponents, localProjectKey, connection, nodeHelper) } static List receive(boolean matchByName, List remoteComponents, String localProjectKey, IConnection connection, NodeHelper nodeHelper) { def pcm = com.atlassian.jira.component.ComponentAccessor.projectComponentManager def pm = com.atlassian.jira.component.ComponentAccessor.projectManager def um = com.atlassian.jira.component.ComponentAccessor.userManager try { def p = pm.getProjectObjByKey(localProjectKey) if (p == null) { throw new com.exalate.api.exception.IssueTrackerException("Contact Exalate Support - for some reason the project `$localProjectKey` is not found during remoteComponent sync for remote remoteComponents `${remoteComponents.collect { remoteComponent -> ["name":remoteComponent.name, "id":remoteComponent.id, "projectKey":remoteComponent.projectKey] } }`".toString()) } def localExProject = nodeHelper.getProject(localProjectKey) def storage = getStorage(connection) remoteComponents.collect { remoteComponent -> try { def createComponent = { def localJiraC = pcm.create( //String name, remoteComponent.name, //String description, remoteComponent.description, //String lead, remoteComponent.lead ? nodeHelper.getUserByEmail(remoteComponent.lead.email)?.key : null, //long assigneeType, remoteComponent.assigneeType.code, //Long projectId p.id ) storage.create(localJiraC.id as String, remoteComponent.id as String) nodeHelper.getComponent(remoteComponent.name, localExProject) as com.exalate.basic.domain.hubobject.v1.BasicHubComponent } def updateComponent = { com.atlassian.jira.bc.project.component.ProjectComponent localJiraC -> def lead = localJiraC.lead ? um.getUserByKey(localJiraC.lead) : null def isNameDiff = remoteComponent.name != localJiraC.name def isDescriptionDiff = remoteComponent.description != localJiraC.description def isLeadDiff = remoteComponent.lead?.email != lead?.emailAddress def isAssigneeTypeDiff = remoteComponent.assigneeType?.code != localJiraC.assigneeType if (!(isNameDiff || isDescriptionDiff || isLeadDiff || isAssigneeTypeDiff)) { // don't update the component return } def _MutableProjectComponent = com.atlassian.jira.component.ComponentAccessor.classLoader.loadClass("com.atlassian.jira.bc.project.component.MutableProjectComponent") def mpc = null try { mpc = _MutableProjectComponent.copy([localJiraC] as Object[]) } catch (groovy.lang.MissingMethodException | NoSuchMethodException ignore) { _MutableProjectComponent.copy(localJiraC) } if (isNameDiff) { mpc.name = remoteComponent.name } if (isDescriptionDiff) { mpc.description = remoteComponent.description } if (isLeadDiff) { mpc.lead = remoteComponent.lead ? nodeHelper.getUserByEmail(remoteComponent.lead.email)?.key : null } if (isAssigneeTypeDiff) { mpc.assigneeType = remoteComponent.assigneeType?.code } pcm.update(mpc) } def mapping = storage.getByRemote(remoteComponent.id as String) if (mapping) { def localJiraC = pcm.getProjectComponent(mapping.local as Long) if (localJiraC == null) { storage.delete(mapping) createComponent() } else { updateComponent(localJiraC) } } else { if (matchByName) { def localComponentId = nodeHelper.getComponent(remoteComponent.name, nodeHelper.getProject(localProjectKey))?.id as Long if (localComponentId != null) { def localJiraV = pcm.getProjectComponent(localComponentId) if (localJiraV) { updateComponent(localJiraV) } else { createComponent() } } else { createComponent() } } else { createComponent() } } nodeHelper.getComponent(remoteComponent.name, nodeHelper.getProject(localProjectKey)) as com.exalate.basic.domain.hubobject.v1.BasicHubComponent } catch (IssueTrackerException ite) { throw ite } catch (Exception e) { throw new com.exalate.api.exception.IssueTrackerException("Contact Exalate Support - failed to receive for remote remoteComponents `${["name":remoteComponent.name, "id":remoteComponent.id, "projectKey":remoteComponent.projectKey] }` for local `$localProjectKey`".toString(), e) } } } catch (com.exalate.api.exception.IssueTrackerException ite) { throw ite } catch (Exception e) { throw new com.exalate.api.exception.IssueTrackerException("Contact Exalate Support - failed to receive for remote remoteComponents `${remoteComponents.collect { remoteComponent -> ["name":remoteComponent.name, "id":remoteComponent.id, "projectKey":remoteComponent.projectKey] } }` for local `$localProjectKey`".toString(), e) } } private static class ExalateComponentEventListener { static LOG = org.slf4j.LoggerFactory.getLogger("com.exalate.scripts.ComponentSync.ExalateComponentEventListener") static String getKey(Integer connectionId) { "com.exalate.scripts.ExalateComponentEventListener.connections.$connectionId".toString() } static listen(Integer connectionId, String connectionName) { def ep = getEp() def entries = ep.listenerInvokers.entries() def listenerKey = getKey(connectionId) def exalateComponentListeners = entries.findAll { listenerInvokerEntry -> def listenerInvoker = listenerInvokerEntry.value listenerInvoker.key == listenerKey } LOG.trace(""" The number listeners for key `$listenerKey`: ${exalateComponentListeners.size()} """.toString()) if (exalateComponentListeners.size() < 1) { ep.registerListener(listenerKey, new ExalateComponentEventListener(connectionId, connectionName)) } } static unlisten(Integer connectionId) { def listenerKey = getKey(connectionId) def ep = getEp() ep.unregisterListener(listenerKey) } private static com.atlassian.event.api.EventPublisher getEp() { com.atlassian.jira.component.ComponentAccessor.getComponent(com.atlassian.event.api.EventPublisher.class) } final Integer connectionId final String connectionName final String listenerKey ExalateComponentEventListener(Integer connectionId, String connectionName) { this.connectionId = connectionId this.connectionName = connectionName this.listenerKey = getKey(connectionId) } @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 ExalateComponentEventListener. """.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 } } //whenever a component gets updated, find the first issue under sync by connection //trigger sync for it final def jiraCl = com.atlassian.jira.component.ComponentAccessor.classLoader def componentEventClazz = jiraCl.loadClass("com.atlassian.jira.event.bc.project.component.AbstractProjectComponentEvent") if (componentEventClazz == null) { LOG.debug("!Stopping to track component events, since ExalateComponentEventListener failed to get the com.atlassian.jira.event.bc.project.component.AbstractProjectComponentEvent class!".toString()) unregister() return } // determine if this event is a ProjectComponentEvent if (componentEventClazz.isAssignableFrom(jiraEventObj.getClass())) { def component = jiraEventObj.projectComponent as com.atlassian.jira.bc.project.component.ProjectComponent //it is proved to be a component event handleComponentEvent(component) } else { if (LOG.isTraceEnabled()) { LOG.trace(""" `${jiraEventObj.class}` is not instance of `com.atlassian.jira.event.bc.project.component.AbstractProjectComponentEvent`: jiraEventObj=`$jiraEventObj` jiraEventObj.dump()=`${jiraEventObj.dump()}` jiraEventObj.class=`${jiraEventObj.class}` jiraEventObj.params=`${(jiraEventObj instanceof com.atlassian.jira.event.JiraEvent) ? (jiraEventObj as com.atlassian.jira.event.JiraEvent).params : "" }` """.toString()) } } } 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 ExalateComponentEventListener from ComponentSync.groovy had a problem: `${e.message}` Please contact Exalate support team, changes to sprints would only be synchronized if changes to the issues within these sprints are performed. """.toString(), e) try { unregister() } catch (Exception ignore) { } } } private void unregister(){ def ep = com.atlassian.jira.component.ComponentAccessor.getComponent(com.atlassian.event.api.EventPublisher.class) ep.unregisterListener(listenerKey) } private void handleComponentEvent(com.atlassian.jira.bc.project.component.ProjectComponent component) { def nserv = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.node.INodeService.class) def sserv = com.atlassian.jira.component.ComponentAccessor.getComponent(com.atlassian.jira.bc.issue.search.SearchService.class) 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 { // 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()) } } def search = { String jql -> //get board query def queryRes = parseQuery(proxyAppUser, jql) if (!queryRes.valid) { LOG.error(""" !Stopping to track sprint events, since ExalateSprintEventListener failed to search for issues in sprint! Failed to parse JQL `$jql`: `${queryRes.errors.errorMessages}`. Please review the script.""".toString()) unregister() throw new Exception("Failed to parse JQL `$jql`: `${queryRes.errors.errorMessages}`. Please review the script.".toString()) } def query = queryRes.query // find issues on board def sRes = searchOverrideSecurity(proxyAppUser, query) def issues = sRes.issues def issuekeys = issues.collect { new com.exalate.basic.domain.BasicIssueKey( it.id as Long, it.key ) } issuekeys } def getIssuesWithComponent = { if (component == null) { return [] } def _SystemSearchConstants = com.atlassian.jira.component.ComponentAccessor.classLoader.loadClass("com.atlassian.jira.issue.search.constants.SystemSearchConstants") def projectFieldName = _SystemSearchConstants.forProject().getJqlClauseNames().getPrimaryName() def componentJqlFieldName = _SystemSearchConstants.forComponent().getJqlClauseNames().getPrimaryName() final def jql = "${projectFieldName} = ${component.projectId} AND ${componentJqlFieldName} = ${component.id}".toString() search(jql) } def issuesWithComponent = getIssuesWithComponent(component) syncFirstIssue(issuesWithComponent) } private def syncFirstIssue(List issues) { 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 = ExalateApi.getConnection(connectionId) if (con) { issues.inject(null) { result, ik -> if (result != null) return result else { def tt = ttrepo.getTwinTraceByLocalIssueKey( con, ik ) if (tt != null) { ess.scheduleSyncEvent(ik, tt) } tt } } } else { LOG.debug("The connection `${connectionName}` ($connectionId) seems to be gone now. Stopping the component sync listener!".toString()) unregister() } } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~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 } } private static class MappingStorageSupplier { static MappingStorage get(String key) { def _PluginSettingsFactory = ComponentAccessor.classLoader.loadClass("com.atlassian.sal.api.pluginsettings.PluginSettingsFactory") def psf = ComponentAccessor.getOSGiComponentInstanceOfType(_PluginSettingsFactory) def collectionPs = psf.createSettingsForKey(key) def metadataPs = psf.createSettingsForKey("${key}._metadata") new MappingStorage(collectionPs, metadataPs) } static class Jsonifier { static JsonSlurper r = new JsonSlurper() static JsonOutput w = new JsonOutput() static String toJson(V v) { w.toJson(v) } static V fromJson(String json, Closure toVFn) { toVFn(r.parseText(json)) } } public static class MappingStorage { private static final String REMOTE_IDS_METADATA_KEY = "remoteIds" private def collectionPs private def metadataPs MappingStorage (collectionPs, metadataPs) { this.collectionPs = collectionPs this.metadataPs = metadataPs } List getAll() { def remoteIds = getRemoteIds() remoteIds.collect { String id -> getByRemote(id) } } Mapping getByLocal (String local) { getAll().find { m -> m.local == local } } Mapping getByRemote (String remote) { def remoteStr = Jsonifier.toJson(new Mapping.RemoteProjection(remote)) def mStr = collectionPs.get(remoteStr) as String if (mStr == null) { return null } Jsonifier.fromJson(mStr, { Map mJson -> new Mapping(mJson) }) } Mapping create (String local, String remote) { def m = new Mapping(local, remote) def remoteStr = Jsonifier.toJson(m.remoteFn()) def mStr = Jsonifier.toJson(m) def mByRemote = getByRemote(remoteStr) if (mByRemote == null) { collectionPs.put(remoteStr, mStr) } addRemoteId(remote) m } void delete (Mapping m) { def remoteStr = Jsonifier.toJson(m.remoteFn()) collectionPs.remove(remoteStr) removeRemoteId(m.remote) } void deleteByLocal (String local) { def m = getByLocal(local) delete(m) } void deleteByRemote (String remote) { def m = getByRemote(remote) delete(m) } private List getRemoteIds() { def localIdsStr = metadataPs.get(REMOTE_IDS_METADATA_KEY) as String def localIds = (localIdsStr ? Jsonifier.fromJson(localIdsStr, { it as List; }) : []) as List; localIds } private List addRemoteId(String remote) { def remoteIds = getRemoteIds() if (!remoteIds.contains(remote)) { remoteIds += remote metadataPs.put(REMOTE_IDS_METADATA_KEY, Jsonifier.toJson(remoteIds)) } remoteIds } private List removeRemoteId(String remote) { def remoteIds = getRemoteIds() def nRemoteIds = remoteIds.size() remoteIds -= remote if (remoteIds.size() != nRemoteIds) { metadataPs.put(REMOTE_IDS_METADATA_KEY, Jsonifier.toJson(remoteIds)) } remoteIds } } static class Mapping { final String local final String remote Mapping(Map map) { this( map.local as String, map.remote as String ) } Mapping(String local, String remote) { this.local = local this.remote = remote } LocalProjection localFn() { new LocalProjection(local) } RemoteProjection remoteFn() { new RemoteProjection(remote) } static class LocalProjection { final String local LocalProjection(String local) { this.local = local } } static class RemoteProjection { final String remote RemoteProjection(String remote) { this.remote = remote } } } } }