Answer by Serhiy Onyshchenko on 19 October 2022
Here’s how one can create comments with properties:
Here’s a modified / validated version of the incoming script:
if(firstSync){
issue.projectKey = "SD"
issue.typeName = nodeHelper.getIssueType(replica.type?.name, issue.projectKey)?.name ?: "Task"
}
issue.summary = replica.summary
issue.description = replica.description
issue.attachments = attachmentHelper.mergeAttachments(issue, replica)
issue.labels = replica.labels
//issue.comments = commentHelper.mergeComments(issue, replica)if (firstSync) { store(issue) }
def js = new groovy.json.JsonSlurper()
store(issue)
def propertyMap = [
"key": "createdByExalate",
"value": [ "v" : "true" ]
]
def propertylist=[propertyMap]
replica.addedComments.each { remoteComment ->
def bodyStr = groovy.json.JsonOutput.toJson(
[body: remoteComment.body, properties: propertylist]
)
//debug.error(""+bodyStr+"--"+issue.key)
def res = new JiraClient(httpClient, injector, debug)
.http(
"POST",
"/rest/api/2/issue/${issue.key}/comment",
[:],
bodyStr,
["Content-Type": ["application/json"]]) { response ->
if (response.code >= 400) debug.error("POST /rest/api/2/issue/${issue.key}/comment failed: ${response.body}")
else {
//store(issue)
//debug.error(remoteComment.body+"---"+issue.comments.last())
//debug.error(""+replica.addedComments)
def json = js.parseText(response.body)
def localCommentId = json.id
def trace = new com.exalate.basic.domain.BasicNonPersistentTrace()
.setType(com.exalate.api.domain.twintrace.TraceType.COMMENT)
.setToSynchronize(true)
.setLocalId(localCommentId as String)
.setRemoteId(remoteComment.remoteId as String)
.setAction(com.exalate.api.domain.twintrace.TraceAction.NONE)
traces.add(trace)
}
}
}
return new scala.Tuple2(issueKey, scala.collection.JavaConverters.asScalaBuffer(traces))
class JiraClient {
// SCALA HELPERS
private static <T> T await(scala.concurrent.Future<T> f) { scala.concurrent.Await$.MODULE$.result(f, scala.concurrent.duration.Duration$.MODULE$.Inf()) }
private static <T> T orNull(scala.Option<T> opt) { opt.isDefined() ? opt.get() : null }
private static <T> scala.Option<T> none() { scala.Option$.MODULE$.<T>empty() }
@SuppressWarnings("GroovyUnusedDeclaration")
private static <T> scala.Option<T> none(Class<T> evidence) { scala.Option$.MODULE$.<T>empty() }
private static <L, R> scala.Tuple2<L, R> pair(L l, R r) { scala.Tuple2$.MODULE$.<L, R>apply(l, r) }
// SERVICES AND EXALATE API
private def getGeneralSettings() {
def gsp = injector.instanceOf(com.exalate.api.persistence.issuetracker.jcloud.IJCloudGeneralSettingsPersistence.class)
def gsOpt = await(gsp.get())
def gs = orNull(gsOpt)
gs
}
private String getJiraCloudUrl() {
final def gs = getGeneralSettings()
def removeTailingSlash = { String str -> str.trim().replace("/+\$", "") }
final def jiraCloudUrl = removeTailingSlash(gs.issueTrackerUrl)
jiraCloudUrl
}
private httpClient
private injector
private debug
def parseQueryString = { String string ->
string.split('&').collectEntries{ param ->
param.split('=', 2).collect{ URLDecoder.decode(it, 'UTF-8') }
}
}
//Usage examples: https://gist.github.com/treyturner/4c0f609677cbab7cef9f
def parseUri
{
parseUri = { String uri ->
def parsedUri
try {
parsedUri = new URI(uri)
if (parsedUri.scheme == 'mailto') {
def schemeSpecificPartList = parsedUri.schemeSpecificPart.split('\\?', 2)
def tempMailMap = parseQueryString(schemeSpecificPartList[1])
parsedUri.metaClass.mailMap = [
recipient: schemeSpecificPartList[0],
cc : tempMailMap.find { it.key.toLowerCase() == 'cc' }.value,
bcc : tempMailMap.find { it.key.toLowerCase() == 'bcc' }.value,
subject : tempMailMap.find { it.key.toLowerCase() == 'subject' }.value,
body : tempMailMap.find { it.key.toLowerCase() == 'body' }.value
]
}
if (parsedUri.fragment?.contains('?')) { // handle both fragment and query string
parsedUri.metaClass.rawQuery = parsedUri.rawFragment.split('\\?')[1]
parsedUri.metaClass.query = parsedUri.fragment.split('\\?')[1]
parsedUri.metaClass.rawFragment = parsedUri.rawFragment.split('\\?')[0]
parsedUri.metaClass.fragment = parsedUri.fragment.split('\\?')[0]
}
if (parsedUri.rawQuery) {
parsedUri.metaClass.queryMap = parseQueryString(parsedUri.rawQuery)
} else {
parsedUri.metaClass.queryMap = null
}
if (parsedUri.queryMap) {
parsedUri.queryMap.keySet().each { key ->
def value = parsedUri.queryMap[key]
if (value.startsWith('http') || value.startsWith('/')) {
parsedUri.queryMap[key] = parseUri(value)
}
}
}
} catch (e) {
throw new com.exalate.api.exception.IssueTrackerException("Parsing of URI failed: $uri\n$e", e)
}
parsedUri
}
}
JiraClient(httpClient, injector, debug) {
this.httpClient = httpClient
this.injector = injector
this.debug = debug
}
String http(String method, String path, java.util.Map<String, List<String>> queryParams, String body, java.util.Map<String, List<String>> headers) {
http(method, path, queryParams, body, headers) { Response response ->
if (response.code >= 300) {
throw new com.exalate.api.exception.IssueTrackerException("Failed to perform the request $method $path (status ${response.code}), and body was: \n```$body```\nPlease contact Exalate Support: ".toString() + response.body)
}
response.body as String
}
}
public <R> R http(String method, String path, java.util.Map<String, List<String>> queryParams, String body, java.util.Map<String, List<String>> headers, Closure<R> transformResponseFn) {
def gs = getGeneralSettings()
def unsanitizedUrl = jiraCloudUrl + path
def parsedUri = parseUri(unsanitizedUrl)
def embeddedQueryParams = parsedUri.queryMap
def allQueryParams = embeddedQueryParams instanceof java.util.Map ?
({
def m = [:] as java.util.Map<String, List<String>>;
m.putAll(embeddedQueryParams as java.util.Map<String, List<String>>)
m.putAll(queryParams)
})()
: (queryParams ?: [:] as java.util.Map<String, List<String>>)
def urlWithoutQueryParams = { String url ->
URI uri = new URI(url)
new URI(uri.getScheme(),
uri.getUserInfo(), uri.getHost(), uri.getPort(),
uri.getPath(),
null, // Ignore the query part of the input url
uri.getFragment()).toString()
}
def sanitizedUrl = urlWithoutQueryParams(unsanitizedUrl)
def response
try {
def request = httpClient
.ws()
.url(sanitizedUrl)
.withMethod(method)
if (headers != null && !headers.isEmpty()) {
def scalaHeaders = scala.collection.JavaConversions.asScalaBuffer(
headers.entrySet().inject([] as List<scala.Tuple2<String, String>>) { List<scala.Tuple2<String, String>> result, kv ->
kv.value.each { v -> result.add(pair(kv.key, v) as scala.Tuple2<String, String>) }
result
}
)
request = request.withHeaders(scalaHeaders)
}
if (!allQueryParams.isEmpty()) {
def scalaQueryParams = scala.collection.JavaConversions.asScalaBuffer(allQueryParams.entrySet().inject([] as List<scala.Tuple2<String, String>>) { List<scala.Tuple2<String, String>> result, kv ->
kv.value.each { v -> result.add(pair(kv.key, v) as scala.Tuple2<String, String>) }
result
})
request = request.withQueryString(scalaQueryParams)
}
if (body != null) {
def writable = play.api.libs.ws.WSBodyWritables$.MODULE$.writeableOf_String()
request = request.withBody(body, writable)
}
request = await(httpClient.authenticate(
none(),
request,
gs
))
def authEntry = scala.collection.JavaConverters
.mapAsJavaMap(request.headers())
.entrySet()
.find { kv -> "authorization".equalsIgnoreCase(kv.key) }
//debug.error("curl -X ${request.method()} -H '${authEntry.key}: ${authEntry.value.head}' -d '${body}' '${request.url()}${({def qpStr = allQueryParams.inject(""){res, kvs -> res + kvs.collect { k, vs -> vs.collect { v -> "$k=$v" }.join("&"); (qpStr.isEmpty())? "" : "?$qpStr" }}})()}'")
response = await(request.execute())
} catch (Exception e) {
throw new com.exalate.api.exception.IssueTrackerException("Unable to perform the request $method $path with body: \n```$body```\n, please contact Exalate Support: ".toString() + e.message, e)
}
java.util.Map<String, List<String>> javaMap = [:]
for (scala.Tuple2<String, scala.collection.Seq<String>> headerTuple : scala.collection.JavaConverters.bufferAsJavaListConverter(response.allHeaders().toBuffer()).asJava()) {
def javaList = []
javaList.addAll(scala.collection.JavaConverters.bufferAsJavaListConverter(headerTuple._2().toBuffer()).asJava())
javaMap[headerTuple._1()] = javaList
}
def javaResponse = new Response(response.body(), new Integer(response.status()), javaMap)
return transformResponseFn(javaResponse)
}
public static class Response {
final String body
final Integer code
final java.util.Map<String, List<String>> headers
Response(String body, Integer code, java.util.Map<String, List<String>> headers) {
this.body = body
this.code = code
this.headers = headers
}
}
}
Regards, Serhiy.
Comments:
Charlie Misonne commented on 20 October 2022
Hi Serhiy,
Thanks for your answer and that script.
Creating the comments via the REST API is too complex for what I need.
Like you suggested I am now filtrering on the comment author in my automation rules instead of using the customer property.
Still, in the future it might be nice if Exalate could support comment properties without using the REST API.
kind regards,
Charlie