import com.exalate.api.domain.connection.IConnection import com.exalate.basic.domain.hubobject.v1.BasicHubCustomField import com.exalate.basic.domain.hubobject.v1.BasicHubIssue import com.exalate.api.domain.hubobject.v1_2.HubCustomFieldType import com.exalate.basic.domain.hubobject.v1.BasicHubOption import com.exalate.basic.domain.hubobject.v1.BasicHubUser import com.exalate.basic.domain.hubobject.v1.BasicHubVersion import com.atlassian.jira.util.thread.JiraThreadLocalUtils import static com.exalate.api.domain.hubobject.v1_2.HubCustomFieldType.* /** Usage: Add the snippet below to the end of your "Outgoing sync(data filter)": Add the snippet below to the your "Incoming sync for new issues(create processor)": CustomFieldSync.receive( CustomFieldSync.logOnNoCfFound, CustomFieldSync.logOnCfContextMismatch, CustomFieldSync.defaultOnNoOptionFound, CustomFieldSync.defaultOnNoUserFound, CustomFieldSync.defaultNoVersionFound, CustomFieldSync.defaultCustomFieldTypeMismatch, [:], [], replica, issue, connection, nodeHelper ) -------------------------------- Add the snippet below to your "Incoming sync for existing issues(change processor)": CustomFieldSync.receive( CustomFieldSync.logOnNoCfFound, CustomFieldSync.logOnCfContextMismatch, CustomFieldSync.defaultOnNoOptionFound, CustomFieldSync.defaultOnNoUserFound, CustomFieldSync.defaultNoVersionFound, CustomFieldSync.defaultCustomFieldTypeMismatch, [:], [], replica, issue, connection, nodeHelper ) -------------------------------- * */ class CustomFieldSync { static LOG = org.slf4j.LoggerFactory.getLogger("com.exalate.scripts.CustomFieldSync") static BasicHubIssue send(BasicHubIssue replica, BasicHubIssue issue) { send([], replica, issue) } private static def isCustomFieldRelevantForIssue(final com.atlassian.jira.issue.Issue jiraIssue, final com.atlassian.jira.issue.fields.CustomField customField) { if(jiraIssue == null || customField == null){ return false } com.atlassian.jira.issue.context.IssueContextImpl issueContext = new com.atlassian.jira.issue.context.IssueContextImpl(jiraIssue.getProjectObject(), jiraIssue.getIssueType()) return isCustomFieldRelevantForIssue(issueContext, customField) } private static def isCustomFieldRelevantForIssue(final com.atlassian.jira.issue.context.IssueContextImpl issueContext, final com.atlassian.jira.issue.fields.CustomField customField) { def fcsm = com.atlassian.jira.component.ComponentAccessor.getFieldConfigSchemeManager() return fcsm.isRelevantForIssueContext(issueContext, customField) } static BasicHubIssue send(List excludeCustomFields, BasicHubIssue replica, BasicHubIssue issue) { def fieldManager = com.atlassian.jira.component.ComponentAccessor.getFieldManager() def nserv = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.node.INodeService.class) def proxyAppUser = nserv.getProxyUser() def jIssue = com.atlassian.jira.component.ComponentAccessor.getIssueManager().getIssueObject(issue.id as Long) replica.id = issue.id replica.key = issue.key JiraThreadLocalUtils.preCall(); try{ def cfs = fieldManager.getAvailableCustomFields(proxyAppUser, jIssue) as List cfs.each { cf -> if (!isCustomFieldRelevantForIssue(jIssue, cf) || !cf.isShown(jIssue) || !cf.hasValue(jIssue) || excludeCustomFields.any { excludeCustomField -> excludeCustomField.equals(cf.name) }) { // we don't do anything with custom fields that are not applicable / relevant for the issue } else { replica.customFields[cf.untranslatedName] = issue.customFields[cf.idAsLong as String] } } }finally{ JiraThreadLocalUtils.postCall(); } return replica } static Closure defaultOnNoCfFound = { customFieldKey, String desiredProjectKey, String desiredIssueTypeName, BasicHubCustomField remoteCustomField -> throw new IllegalStateException( "Unable to find `$customFieldKey` custom field for project `${desiredProjectKey}` and issue type `${desiredIssueTypeName}`. ".toString()+ "Either it has been removed, or the name has been changed. Please, check it." ) } static Closure defaultOnCfContextMismatch = { customFieldKey, String desiredProjectKey, String desiredIssueTypeName, BasicHubCustomField remoteCustomField, List fieldConfigSchemes -> throw new IllegalStateException( """`$customFieldKey` custom field is not applicable for project `${desiredProjectKey}` and issue type `${desiredIssueTypeName}`. Schemes: `${fieldConfigSchemes.collect { cfsch -> [ "name":cfsch.name, "global" : cfsch.global, "allproj" : cfsch.allProjects, "allIts" : cfsch.allIssueTypes, "ctxs": cfsch.contexts.join(", "), "enabled" : cfsch.enabled, "associatedIssueTypeIds" : cfsch.associatedIssueTypeIds] }}` Either it has been removed, or the name has been changed. Please, check it.""".toString() ) } static Closure logOnCfContextMismatch = { customFieldKey, String desiredProjectKey, String desiredIssueTypeName, BasicHubCustomField remoteCustomField, List fieldConfigSchemes -> LOG.error( """`$customFieldKey` custom field is not applicable for project `${desiredProjectKey}` and issue type `${desiredIssueTypeName}`. Schemes: `${fieldConfigSchemes.collect { cfsch -> [ "name":cfsch.name, "global" : cfsch.global, "allproj" : cfsch.allProjects, "allIts" : cfsch.allIssueTypes, "ctxs": cfsch.contexts.join(", "), "enabled" : cfsch.enabled, "associatedIssueTypeIds" : cfsch.associatedIssueTypeIds] }}` Either it has been removed, or the name has been changed. Please, check it.""".toString() ) null } static Closure logOnNoCfFound = { customFieldKey, String desiredProjectKey, String desiredIssueTypeName, BasicHubCustomField remoteCustomField -> LOG.error("""Unable to find `$customFieldKey` custom field for project `${desiredProjectKey}` and issue type `${desiredIssueTypeName}`. Either it has been removed, or the name has been changed. Please, check it.""".toString()) null } static Closure defaultOnNoOptionFound = { String value, id, String localCfName, cfId, List> allOptMaps, BasicHubOption remoteOption -> throw new IllegalStateException( "Unable to find `" + value + "` ("+ id +") option for custom field `"+ localCfName +"` ("+ cfId +"). "+ "Either it has been removed, or the name has been changed. " + "Known options: `"+ allOptMaps +"` " + "Please, check it." ) } static Closure defaultOnNoUserFound = { String remoteExUserEmail, String remoteExUserUsername, String remoteExUserDisplayName, BasicHubUser remoteUser -> throw new com.exalate.api.exception.IssueTrackerException( "User with email " + "`"+ remoteExUserEmail + "` and username `"+ remoteExUserUsername +"` and Full Name: `"+ remoteExUserDisplayName +"`" + "is not present on this Jira (searched by username), create one, or change the script to assign replacement user for this one" ) } static Closure defaultNoVersionFound = { String versionName, id, String localCfName, cfId, List> allOptMaps, String projectName, String projectKey, projectId, BasicHubVersion remoteVersion -> throw new IllegalStateException( """Unable to find `$versionName` ($id) version in project $projectName ($projectKey) to set as a value for custom field `$localCfName` ($cfId). Either it has been removed, or the name has been changed. Known versions for $projectName ($projectKey): `$allOptMaps` Please, check it.""".toString() ) } static Closure defaultCustomFieldTypeMismatch = { BasicHubCustomField localCustomField, BasicHubCustomField remoteCustomField -> throw new IllegalStateException( """Types for local cusom field `${localCustomField.name}` (${localCustomField.id}) is different from remote custom field `${remoteCustomField.name}` (${remoteCustomField.id}). Please, check it.""".toString() ) } static BasicHubIssue receive(BasicHubIssue replica, BasicHubIssue issue, IConnection connection, com.exalate.node.hubobject.v1_3.NodeHelper nodeHelper) { receive(defaultOnNoCfFound, defaultOnCfContextMismatch, defaultOnNoOptionFound, defaultOnNoUserFound, defaultNoVersionFound, null, [], replica, issue, connection, nodeHelper) } static BasicHubIssue receive(List excludeCustomFields, BasicHubIssue replica, BasicHubIssue issue, IConnection connection, com.exalate.node.hubobject.v1_3.NodeHelper nodeHelper) { receive(defaultOnNoCfFound, defaultOnCfContextMismatch, defaultOnNoOptionFound, defaultOnNoUserFound, defaultNoVersionFound, null, excludeCustomFields, replica, issue, connection, nodeHelper) } static BasicHubIssue receive( Closure onNoCfFound, Closure onCfContextMismatch, Closure onNoOptionFound, Closure onNoUserFound, Closure onNoVersionFound, Map customFieldMapping, List excludeCustomFields, BasicHubIssue replica, BasicHubIssue issue, IConnection connection, com.exalate.node.hubobject.v1_3.NodeHelper nodeHelper) { receive(onNoCfFound, onCfContextMismatch, onNoOptionFound, onNoUserFound, onNoVersionFound, defaultCustomFieldTypeMismatch, customFieldMapping, excludeCustomFields, replica, issue, connection, nodeHelper) } static BasicHubIssue receive( Closure onNoCfFound, Closure onCfContextMismatch, Closure onNoOptionFound, Closure onNoUserFound, Closure onNoVersionFound, Closure onTypesMismatch, Map customFieldMapping, List excludeCustomFields, BasicHubIssue replica, BasicHubIssue issue, IConnection connection, com.exalate.node.hubobject.v1_3.NodeHelper nodeHelper) { final def cfm = com.atlassian.jira.component.ComponentAccessor.getCustomFieldManager() final def pm = com.atlassian.jira.component.ComponentAccessor.getProjectManager() final def itm = com.atlassian.jira.component.ComponentAccessor.getComponent(com.atlassian.jira.config.IssueTypeManager.class) final def om = com.atlassian.jira.component.ComponentAccessor.getOptionsManager() final def nhocs = nodeHelper.nodeHubObjectConverter def desiredProjectKey = issue.projectKey ?: issue.project?.key def desiredProject = pm.getProjectObjByKey(desiredProjectKey) def desiredIssueTypeName = issue.typeName ?: issue.type?.name def desiredIssueType = itm.getIssueTypes().find { desiredIssueTypeName.equalsIgnoreCase(it.name) } def issueContext = new com.atlassian.jira.issue.context.IssueContextImpl(desiredProject, desiredIssueType) def cfMapping = customFieldMapping ?: [ : ] excludeCustomFields = excludeCustomFields.findAll() def getJiraCustomField = { String cfName -> // def cfs = cfm.getCustomFieldObjectsByName(cfMapping[cfName] ?: cfName) def name = cfMapping[cfName] ?: cfName def allCfs = cfm.getCustomFieldObjects() def cfs = allCfs .findAll { cf -> cf.untranslatedName == name } // def p = desiredProject // def it = desiredIssueType // throw new com.exalate.api.exception.IssueTrackerException("""DEBUG: //cfName=`${cfName}` //projectKey=`${projectKey}` //issueTypeName=`${issueTypeName}` //``` //allCfs // .findAll { cf -> cf.untranslatedName == name } // .findAll { cf -> !excludeCustomFields.any { it.equals(cf.name) } } //```=`${cfs}` //`cfs.collect { aCf -> ... ConfigurationSchemes ... }`=`${cfs.collect { aCf -> aCf.getConfigurationSchemes().collect { cfsch -> [ "name":cfsch.name, "global" : cfsch.global, "allproj" : cfsch.allProjects, "allIts" : cfsch.allIssueTypes, "ctxs": cfsch.contexts.join(", "), "enabled" : cfsch.enabled, "associatedIssueTypeIds" : cfsch.associatedIssueTypeIds] } }}` //`cfs.findAll { aCf -> aCf.isGlobal() || aCf.isInScope(p, [it.id]) }`=`${cfs.findAll { aCf -> aCf.isGlobal() || aCf.isInScope(p, [it.id]) }}` //""".toString()) // cfs = cfs.findAll { aCf -> aCf.isGlobal() || aCf.isInScope(p, [it.id]) } cfs.find() } def getExCustomField = { com.atlassian.jira.issue.fields.CustomField jiraCf -> nodeHelper.getCustomField(jiraCf?.idAsLong) as BasicHubCustomField } def getExCustomFieldOption = { Long jCfId, v -> def jCf = cfm.getCustomFieldObject(jCfId) def fieldConfig = jCf.getRelevantConfig(issueContext) def allOptions = om.getOptions(fieldConfig) def jOptMaps = allOptions.collect { o -> [ "found":o.value == v.value, "`o.value`":o.value, "`v.value`": v.value, "`o.value?.class`":o.value?.class, "`v.value?.class`":v.value?.class, "opt":o ] } def jOpt = jOptMaps.find {x -> x.found}?.opt if (jOpt != null){ nhocs.getHubOption(jOpt) } else { null } } def getLocalExUserOrNone = { BasicHubUser remoteExUser -> def remoteCriteria = remoteExUser?.username nodeHelper.getUserByUsername(remoteCriteria) } issue.customFields = replica.customFields.entrySet().inject(issue.customFields) { Map cfs, kv -> def k = kv.key def cf = kv.value def v = cf.value if (excludeCustomFields.contains(cf.name)) { LOG.debug("Skipping the sync for custom field `${cf.name}` since it's one of the excluded custom fields: `${excludeCustomFields.join("`,`")}`".toString()) return cfs } def localJiraCf = getJiraCustomField(k) def localCf = getExCustomField(localJiraCf) if (localCf == null) { localCf = onNoCfFound(k, desiredProjectKey, desiredIssueTypeName, cf) } if (localCf == null) { LOG.debug("Skipping the sync for custom field `${cf.name}` since onNoCfFound returned null".toString()) return cfs } if (v!= null && !localJiraCf.isGlobal() && !localJiraCf.isInScope(desiredProject, [desiredIssueType.id])) { localCf = onCfContextMismatch(k, desiredProjectKey, desiredIssueTypeName, cf, localJiraCf.configurationSchemes) } if (localCf == null) { LOG.debug("Skipping the sync for custom field `${cf.name}` since onCfContextMismatch returned null".toString()) return cfs } if (!isCustomFieldRelevantForIssue(issueContext, localJiraCf)) { LOG.debug("Skipping the sync for custom field `${cf.name}` (${localJiraCf.idAsLong}) since it's not applicable for the project: `${issueContext.projectObject?.key}` (${issueContext.projectObject?.id}) and issue type: `${issueContext.issueType?.name}` (${issueContext.issueType?.id}): `${excludeCustomFields.join("`,`")}`".toString()) return cfs } Long cfId = (localCf.id as Long) def getLocalExUser = { BasicHubUser remoteExUser -> def localExUser = getLocalExUserOrNone(remoteExUser) if (localExUser == null) { localExUser = onNoUserFound(remoteExUser?.email, remoteExUser?.username, remoteExUser?.displayName, remoteExUser) if (localExUser == null) { LOG.debug("Skipping the sync for custom field `${cf.name}` and part `${remoteExUser}` of remote value `${v}` since onNoUserFound returned null".toString()) } localExUser } else { localExUser } } if (localCf.type != cf.type) { cf = onTypesMismatch(localCf, cf) } if (cf == null) { LOG.debug("Skipping the sync for custom field `${cf.name}` since onTypesMismatch returned null".toString()) return cfs } if (v == null) { cfs[cfId as String] = localCf cfs[cfId as String].value = null cfs.remove(localCf.name) } else { if (cf.type.name() == OPTION.name()) { def vOpt = getExCustomFieldOption(cfId, v) if (vOpt == null) { def jCf = cfm.getCustomFieldObject(cfId) def fieldConfig = jCf.getRelevantConfig(issueContext) def allOptions = om.getOptions(fieldConfig) def allOptMaps = allOptions.collect { o -> ["id": o.optionId, "value": o.value, "`o.value == v.value`":(o.value == v.value)] } vOpt = onNoOptionFound(v.value, v.id, localCf.name, cfId, allOptMaps, v) } if (vOpt == null) { LOG.debug("Skipping the sync for custom field `${cf.name}` and remote value `${v}` since onNoOptionFound returned null".toString()) return cfs } cfs[cfId as String] = localCf cfs[cfId as String].value = vOpt cfs.remove(localCf.name) } else if (cf.type.name() == OPTIONS.name()) { def localVs = v.collect { remoteV -> def localV = getExCustomFieldOption(cfId, remoteV) if (localV == null) { def jCf = cfm.getCustomFieldObject(cfId) def fieldConfig = jCf.getRelevantConfig(issueContext) def allOptions = om.getOptions(fieldConfig) def allOptMaps = allOptions.collect { o -> ["id": o.optionId, "value": o.value, "`o.value == v.value`":(o.value == remoteV.value)] } localV = onNoOptionFound(remoteV.value, remoteV.id, localCf.name, cfId, allOptMaps, localV) if (localV == null) { LOG.debug("Skipping the sync for custom field `${cf.name}` and remote value `${v}` since onNoOptionFound returned null".toString()) } localV } else { localV } }.findAll() cfs[cfId as String] = localCf cfs[cfId as String].value = localVs cfs.remove(localCf.name) } else if (cf.type.name() == USER.name()) { def vOpt = getLocalExUser(v as BasicHubUser) // UserSync.receive(v as com.exalate.basic.domain.hubobject.v1.BasicHubUser, nodeHelper) cfs[cfId as String] = localCf cfs[cfId as String].value = vOpt cfs.remove(localCf.name) } else if (cf.type.name() == USERS.name()) { def localVs = v.collect(getLocalExUser).findAll() cfs[cfId as String] = localCf cfs[cfId as String].value = localVs cfs.remove(localCf.name) } else if (cf.type.name() == LABELS.name()) { def localVs = v as Set cfs[cfId as String] = localCf cfs[cfId as String].value = localVs cfs.remove(localCf.name) } else if (cf.type.name() == VERSIONS.name()) { def localVs = v.collect { BasicHubVersion version -> def localVersion = nodeHelper.getVersion(version.name, nodeHelper.getProject( issue.project?.key ?: issue.projectKey )) if (localVersion == null) { def proj = pm.getProjectObjByKey(issue.project?.key ?: issue.projectKey) def allVMaps = proj.getVersions().collect { projectVersion -> ["id": projectVersion.id, "value": projectVersion.name, "`o.name == v.name`":(projectVersion.name == version.name)] } localVersion = onNoVersionFound(version.name, version.id, localCf.name, cfId, allVMaps, proj.name, proj.key, proj.id, version) if (localVersion == null) { LOG.debug("Skipping the sync for custom field `${cf.name}` and part `${version}` of remote value `${v}` since onNoOptionFound returned null".toString()) } localVersion } else { localVersion } } cfs[cfId as String] = localCf cfs[cfId as String].value = localVs cfs.remove(localCf.name) } else if ([ STRING, TEXT, NUMERIC, DATE, DATETIME, HubCustomFieldType.URL, ].any { HubCustomFieldType simpleType -> simpleType.name().equals(cf.type?.name()) }) { cfs[cfId as String] = localCf cfs[cfId as String].value = v cfs.remove(localCf.name) } } cfs } issue } static class Break extends Exception { } }