This script finds references to the fields you specify in TARGET_FIELDS in the following locations:
-
Columns in saved custom views. This includes uses of the field in formulas.
-
Generator JQL queries and saved quick transformations across all structures.
Groovy
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.jql.parser.JqlQueryParser
// empty means all fields
def TARGET_FIELDS = [
"<field_ID>",
] as Set
def SHOW_VIEWS = true
def SHOW_TRANSFORMATIONS = true
// if it takes too much time, set SHOW_GENERATORS = false and SHOW_EFFECTORS = false
def SHOW_GENERATORS = true
def SHOW_EFFECTORS = true
def plugin = ComponentAccessor.pluginAccessor.getPlugin('com.almworks.jira.structure')
def loader = plugin.classLoader
def SpecParams = loader.loadClass('com.almworks.jira.structure.api.util.SpecParams')
def ValueFormat = loader.loadClass('com.almworks.jira.structure.api.attribute.ValueFormat')
def AttributeSpec = loader.loadClass('com.almworks.jira.structure.api.attribute.AttributeSpec')
def AttributeUtil = loader.loadClass('com.almworks.structure.commons.util.AttributeUtil')
def StructureUtil = loader.loadClass('com.almworks.jira.structure.api.util.StructureUtil')
def ExprParser = loader.loadClass('com.almworks.jira.structure.expr.parser.ExprParser')
def ExprExecutorUtil = loader.loadClass('com.almworks.jira.structure.expr.executor.ExprExecutorUtil')
def ForestSpec = loader.loadClass('com.almworks.jira.structure.api.forest.ForestSpec')
def ItemAccessMode = loader.loadClass('com.almworks.jira.structure.api.row.ItemAccessMode')
def CoreIdentities = loader.loadClass('com.almworks.jira.structure.api.item.CoreIdentities')
def StructureComponents = loader.loadClass('com.almworks.jira.structure.api.StructureComponents')
def sc = ComponentAccessor.getOSGiComponentInstanceOfType(StructureComponents)
def viewManager = sc.viewManager
def structureManager = sc.structureManager
def propertyService = sc.structurePropertyService
def forestService = sc.forestService
def rowManager = sc.rowManager
def generatorManager = sc.generatorManager
def effectorManager = sc.effectorInstanceManager
def fieldManager = ComponentAccessor.fieldManager
def customFieldManager = ComponentAccessor.customFieldManager
def jqlParser = ComponentAccessor.getComponent(JqlQueryParser)
def specParamsCtor = SpecParams.getConstructor(Map)
def newSpecParams = { Map m -> specParamsCtor.newInstance(m) }
def attrSpecCtor = AttributeSpec.getConstructor(String, ValueFormat, Map)
def valueFormatAny = ValueFormat.ANY
def newExprSpec = { Map p -> attrSpecCtor.newInstance('expr', valueFormatAny, p) }
def skipAccess = ItemAccessMode.SKIP_ACCESS_CHECK
def fieldNameOf = { String id -> def f = id == null ? null : fieldManager.getField(id); f ? f.name : id }
def addField = { Map used, String id ->
if (id == null) return
if (TARGET_FIELDS.isEmpty() || TARGET_FIELDS.contains(id)) used[id] = fieldNameOf(id)
}
def singleParam = { Map m, String k ->
def v = m?.get(k)
(v instanceof List) ? (v.isEmpty() ? null : v[0]) : v
}
def resolveFieldByName = { String token ->
if (token == null) return null
def name = ExprExecutorUtil.canonicalName(token)
def fid = ExprExecutorUtil.convertExprFieldNameToFieldId(name)
if (fieldManager.getField(fid) != null) return fid
def norm = ExprExecutorUtil.normalizeName(token)
def matches = customFieldManager.getCustomFieldObjects().findAll {
ExprExecutorUtil.normalizeName(it.name) == norm
}
if (matches.isEmpty()) return null
matches = matches.sort { a, b -> (a.name == token ? 0 : 1) <=> (b.name == token ? 0 : 1) ?: (a.idAsLong <=> b.idAsLong) }
"customfield_" + matches[0].idAsLong
}
def resolveJqlFieldName = { String nm ->
def m = (nm =~ /(?i)^cf[(\d+)]$/)
if (m.find()) { def id = "customfield_" + m.group(1); return fieldManager.getField(id) ? id : null }
resolveFieldByName(nm)
}
def walk
walk = { node, List locals, Set usedVars, Set accessNames ->
if (node == null) return
switch (node.class.simpleName) {
case 'Function':
def fn = node.functionName
def args = node.arguments
if (fn == 'access' && args != null && args.size() == 2 && args[1].class.simpleName == 'Literal') {
def val = args[1].value
if (val != null && val.class.simpleName == 'StringExprValue')
accessNames << ExprExecutorUtil.canonicalName(val.getString())
}
args?.each { walk(it, locals, usedVars, accessNames) }
break
case 'Invocation':
walk(node.target, locals, usedVars, accessNames)
node.arguments?.each { walk(it, locals, usedVars, accessNames) }
break
case 'Variable':
if (!locals.contains(node.name)) usedVars << node.name
break
case 'VariableDeclaration':
walk(node.value, locals, usedVars, accessNames)
locals.add(node.name)
walk(node.expression, locals, usedVars, accessNames)
locals.remove(locals.size() - 1)
break
case 'Lambda':
def ps = node.parameters
ps.each { locals.add(it) }
walk(node.body, locals, usedVars, accessNames)
ps.each { locals.remove(it) }
break
case 'Aggregation':
def e = node.expression
if (e != null) walk(e, locals, usedVars, accessNames)
break
}
}
def processSpec, scanForNestedSpecs, handleFormula
scanForNestedSpecs = { obj, Map used, int depth, Set seen ->
if (obj instanceof Map) {
if (obj.get('id') instanceof String && obj.get('format') instanceof String) {
def child = newSpecParams([a: obj]).getAttributeParameter('a', null)
if (child != null) { processSpec(child, used, depth + 1, seen); return }
}
obj.values().each { scanForNestedSpecs(it, used, depth, seen) }
} else if (obj instanceof List) {
obj.each { scanForNestedSpecs(it, used, depth, seen) }
}
}
handleFormula = { spec, Map used, int depth, Set seen ->
def params = spec.getParams()
def formula = params.getString('formula')
def bound = [:]
def vars = params.getObject('variables')
if (vars != null) {
vars.keys().each { k ->
def sub = vars.getAttributeParameter(k, null)
if (sub != null) bound[ExprExecutorUtil.canonicalName(k)] = sub
}
}
if (!formula) return
def pr = ExprParser.parse(formula)
if (!pr.isSuccessful() || pr.rootNode == null) return
def usedVars = [] as Set, accessNames = [] as Set
walk(pr.rootNode, [], usedVars, accessNames)
usedVars.each { vn ->
if (bound.containsKey(vn)) processSpec(bound[vn], used, depth + 1, seen)
else { def fid = resolveFieldByName(vn); if (fid) addField(used, fid) }
}
accessNames.each { an -> def fid = resolveFieldByName(an); if (fid) addField(used, fid) }
}
processSpec = { spec, Map used, int depth, Set seen ->
if (spec == null || depth > 25) return
def sig = spec.id + '|' + spec.paramsMap
if (seen.contains(sig)) return
seen << sig
if (spec.id == 'expr') { handleFormula(spec, used, depth, seen); return }
def fid = AttributeUtil.convertAttributeSpecToIssueFieldId(spec)
if (fieldManager.getField(fid) != null) { addField(used, fid); return }
scanForNestedSpecs(spec.paramsMap ?: [:], used, depth, seen)
}
def moduleShort = { String m -> m == null ? '?' : m.replace('com.almworks.jira.structure:', '') }
def humanKind = { String s ->
if (s.startsWith('grouper')) return 'Group'
if (s.startsWith('sorter')) return 'Sort'
if (s.startsWith('filter')) return 'Filter'
if (s.startsWith('extender'))return 'Extend'
if (s.startsWith('inserter'))return 'Insert'
if (s == 'effector-attribute-to-field') return 'Copy attribute to field'
if (s == 'effector-status-rollup') return 'Status rollup'
if (s == 'effector-conversion') return 'Convert to issue'
s
}
def extractFields
extractFields = { Map params, Map used ->
def seen = [] as Set
scanForNestedSpecs(params ?: [:], used, 0, seen)
def rawFid = singleParam(params, 'fieldId')
if (rawFid instanceof String && fieldManager.getField(rawFid) != null) addField(used, rawFid)
def aj = singleParam(params, 'attributeJson')
if (aj instanceof String && aj.trim()) {
try { scanForNestedSpecs(StructureUtil.fromJson(aj, Map), used, 0, seen) } catch (ignore) {}
}
def jql = singleParam(params, 'jql')
if (jql instanceof String && jql.trim()) {
try {
def q = jqlParser.parseQuery(jql)
def names = [] as Set
def collect
collect = { c ->
if (c == null) return
def subs = c.clauses
if (subs == null || subs.isEmpty()) { try { names << c.name } catch (ignore) {} }
else subs.each { collect(it) }
}
collect(q.whereClause)
q.orderByClause?.searchSorts?.each { names << it.field }
names.each { nm -> def fid = resolveJqlFieldName(nm); if (fid) addField(used, fid) }
} catch (ignore) {}
}
}
def fmtFields = { Map used -> used.isEmpty() ? '(no target fields)' : used.collect { k, v -> "${v} (${k})" }.join(', ') }
def out = new StringBuilder()
def errors = []
if (SHOW_VIEWS) {
out.append("========== VIEWS -> COLUMNS -> FIELDS ==========\n")
def viewHits = 0
viewManager.getViews(null).sort { it.id }.each { view ->
try {
def colLines = []
view.specification.columns.each { col ->
def used = [:]
def seen = [] as Set
def params = col.parameters ?: [:]
try {
def attr = newSpecParams(params).getAttributeParameter(null)
if (attr != null) processSpec(attr, used, 0, seen)
if (params.get('formula') instanceof String) processSpec(newExprSpec(params), used, 0, seen)
def fld = singleParam(params, 'field')
if (fld instanceof String && fld) processSpec(AttributeUtil.convertIssueFieldIdToAttributeSpec(fld, ValueFormat.HTML), used, 0, seen)
scanForNestedSpecs(params, used, 0, seen)
} catch (e) { errors << "view ${view.id} col ${col.csid}: ${e}" }
if (!used.isEmpty()) {
def title = col.name
if (!title) {
def fld = singleParam(params, 'field')
def f = (fld instanceof String) ? fieldManager.getField(fld) : null
title = f ? f.name : col.key
}
colLines << " - column \"${title}\" [csid=${col.csid}, key=${col.key}] -> ${fmtFields(used)}"
}
}
if (!colLines.isEmpty()) {
viewHits++
out.append("- View \"${view.name}\" (id=${view.id})\n")
colLines.each { out.append(it).append('\n') }
}
} catch (e) { errors << "view ${view.id}: ${e}" }
}
out.append("(views with matching columns: ${viewHits})\n\n")
}
if (SHOW_TRANSFORMATIONS) {
out.append("========== STRUCTURES -> SAVED TRANSFORMATION -> GENERATOR -> FIELDS ==========\n")
def structHits = 0
structureManager.getAllStructures(null, true).sort { it.id }.each { structure ->
try {
def encoded = propertyService.getString(structure.id, 'quick-transforms', '[]')
def items = StructureUtil.fromJson(encoded, Map[].class)
if (items == null || items.length == 0) return
def flatten = { itm, groupName ->
def list = []
if (itm?.key == 'group') { (itm.transforms ?: []).each { list << [it, itm.quick?.name] } }
else list << [itm, groupName]
list
}
def trLines = []
items.each { itm ->
flatten(itm, null).each { pair ->
def t = pair[0]; def groupName = pair[1]
def module = t?.module
def params = t?.params ?: [:]
def used = [:]
try { extractFields(params, used) }
catch (e) { errors << "structure ${structure.id} transform ${module}: ${e}" }
def ms = moduleShort(module)
def name = t?.quick?.name ?: humanKind(ms)
def label = groupName ? "[${groupName}] ${name}" : name
if (TARGET_FIELDS.isEmpty() || !used.isEmpty()) trLines << " - \"${label}\" | generator=${ms} -> ${fmtFields(used)}"
}
}
if (!trLines.isEmpty()) {
structHits++
out.append("- Structure \"${structure.name}\" (id=${structure.id})\n")
trLines.each { out.append(it).append('\n') }
}
} catch (e) { errors << "structure ${structure.id}: ${e}" }
}
out.append("(structures with saved transformations: ${structHits})\n\n")
}
def genLines = [:]
def effLines = [:]
if (SHOW_GENERATORS || SHOW_EFFECTORS) {
structureManager.getAllStructures(null, true).sort { it.id }.each { structure ->
try {
def forest = forestService.getForestSource(ForestSpec.skeleton(structure.id)).latest.forest
forest.rows.each { r ->
def row = rowManager.getRow(r.value(), skipAccess)
def itemId = row.itemId
if (SHOW_GENERATORS && CoreIdentities.isGenerator(itemId)) {
try {
def spec = generatorManager.getGenerator(itemId.getLongId())
def used = [:]
extractFields(spec.parameters ?: [:], used)
def ms = moduleShort(spec.moduleKey)
if (TARGET_FIELDS.isEmpty() || !used.isEmpty()) {
genLines.get(structure, []) << " - ${humanKind(ms)} | generator=${ms} -> ${fmtFields(used)}"
}
} catch (e) { errors << "structure ${structure.id} generator ${itemId}: ${e}" }
} else if (SHOW_EFFECTORS && CoreIdentities.isEffector(itemId)) {
try {
def ei = effectorManager.getEffectorInstance(itemId.getLongId())
def used = [:]
extractFields(ei.parameters ?: [:], used)
def ms = moduleShort(ei.moduleKey)
if (ms == 'effector-status-rollup') { addField(used, 'status'); addField(used, 'resolution') }
if (TARGET_FIELDS.isEmpty() || !used.isEmpty()) {
effLines.get(structure, []) << " - ${humanKind(ms)} | effector=${ms} -> ${fmtFields(used)}"
}
} catch (e) { errors << "structure ${structure.id} effector ${itemId}: ${e}" }
}
}
} catch (e) { errors << "structure ${structure.id} forest: ${e}" }
}
}
if (SHOW_GENERATORS) {
out.append("========== STRUCTURES -> INSTALLED GENERATORS -> FIELDS ==========\n")
genLines.keySet().sort { it.id }.each { structure ->
out.append("- Structure \"${structure.name}\" (id=${structure.id})\n")
genLines[structure].each { out.append(it).append('\n') }
}
out.append("(structures with matching generators: ${genLines.size()})\n\n")
}
if (SHOW_EFFECTORS) {
out.append("========== STRUCTURES -> INSTALLED EFFECTORS -> FIELDS ==========\n")
effLines.keySet().sort { it.id }.each { structure ->
out.append("- Structure \"${structure.name}\" (id=${structure.id})\n")
effLines[structure].each { out.append(it).append('\n') }
}
out.append("(structures with matching effectors: ${effLines.size()})\n")
}
if (!errors.isEmpty()) {
out.append("\n========== ERRORS (${errors.size()}) ==========\n")
errors.take(50).each { out.append("! ").append(it).append('\n') }
}
log.info("\n" + out)
out.toString()