howto call a groovy code from Scriptrunner for Confluence macro

Carsten Hilber
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
August 13, 2017

I want to write a macro with Scriptrunner. It should place a button over a table. On pressing that button i want to read some of the fields of that table and send it to a REST Service.

I know how to place that button from the examples provided from Adaptavist. I also know how to write groovy code to have a function to feed the REST Service. 

I struggle where to put the groovy code, so that I can call it, when the button is pressed. Can sonone provide an example, please?

 

1 answer

1 accepted

1 vote
Answer accepted
Jonny Carter
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
November 19, 2017

So, there are a few things you might be asking; I'll try to address each.

Per the documentation on custom macros, there are three code blocks that define your macro.

The first takes some Groovy code that should return an HTML string. The HTML will be what ultimately renders on the page, and in your case, should probably include an <button> tag.

The second takes some JavaScript code. This is where you'd write the code that read the contents of your table and sent the data to an external REST service.

The third block is where you can put CSS, to style your macro.

If you're asking where you put the code for your REST Service, that would be in a custom REST Endpoint, which is a separate piece of configuration from your macro.

Does that make sense?

Jonny Carter
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
November 19, 2017

To expand a bit, the javascript to read your table might look something like this:

AJS.toInit(function () {
let buttonSelector = 'button#myCustomMacroButton';
let button = document.querySelector(buttonSelector);

function clickHandler(clickEvent) {
let tableElements = document.querySelectorAll(buttonSelector + ' ~ table td');
let tableData = [];
tableElements.forEach(function (tdElement) {
tableData.push(tdElement.innerHTML);
});
AJS.$.ajax({
type: "POST",
url : AJS.params.contextPath + "/rest/scriptrunner/latest/custom/myCustomEndpoint",
data: {
tableData: tableData
}
}).done(function (data, status, jqXhr) {
console.log("Retrieved response from custom rest endpoint: ");
console.log(data);
console.log(status);
});
}

button.addEventListener('click', clickHandler);
});

The handler function passed to "done" is what happens whenever your data comes back from the server. I assume you'd want something more than just logging the data to the console, but that's just an example.

Your REST Endpoint would need to receive that data and do something with it.

import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonBuilder
import groovy.transform.BaseScript
import org.codehaus.jackson.map.ObjectMapper
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response

@BaseScript CustomEndpointDelegate delegate

myCustomEndpoint(
httpMethod: "POST", groups: ["confluence-administrators"]
) { MultivaluedMap queryParams, String body ->

def mapper = new ObjectMapper()
def data = mapper.readValue(body, Map)

return Response.ok("Received data: $data").build()
}
Carsten Hilber
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
March 28, 2019

Helped a lot, thanks 

Santosh Subramani June 7, 2019

Hello Carsten Hilber

Can you pass on the code of your request.

My requirement is to display Crowd user information in confluence page. 

Parameter is Jira Project Key. 

Rest api to get user information and display in a page.

Sushma September 20, 2019

Hi @Jonny Carter @Carsten Hilber 

I have a similar requirement, On clicking the "Button" it should trigger a REST api of jira Server and also it shoul send the body of entire page and  pageTitle.

is it possible to do that ?

I tried but, onclick() event is not triggering the REST API of jira

Please respond..

Thanks in advance 

Sushma

Jonny Carter
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
September 20, 2019

@Sushma- easiest way to debug it will probably be to put some console.log("<message") statements into your code at various points to see where execution stops and why using your browser's developer console. I'd also recommend looking in your browser's network tab to see if the call to the REST API ever gets made, and if it returns an error.

That's the best advice I can give you. If you still need help, maybe post your own question with a detailed description of what you're trying to do, why, and what code you're using.

Sushma September 22, 2019

Hi @Jonny Carter 

In jira post function I have added the below code to achieve the page creation in confluence (using page template) from jira server.

import com.atlassian.applinks.api.ApplicationLink
import com.atlassian.applinks.api.ApplicationLinkService
import com.atlassian.applinks.api.application.confluence.ConfluenceApplicationType
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.sal.api.net.Request
import com.atlassian.sal.api.net.Response
import com.atlassian.sal.api.net.ResponseException
import com.atlassian.sal.api.net.ResponseHandler
import groovy.json.JsonBuilder
import groovy.json.JsonSlurper
import groovy.xml.MarkupBuilder
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.bc.issue.search.SearchService
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.web.bean.PagerFilter
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.issue.ModifiedValue
import com.atlassian.jira.issue.util.DefaultIssueChangeHolder
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.issue.search.SearchProvider
import groovy.xml.MarkupBuilder

def ApplicationLink getPrimaryConfluenceLink() {
def applicationLinkService = ComponentLocator.getComponent(ApplicationLinkService.class)
final ApplicationLink conflLink = applicationLinkService.getPrimaryApplicationLink(ConfluenceApplicationType.class)
conflLink
}
HashSet<String> typecomponentset = new HashSet();
HashSet<String> typeflowset = new HashSet();
def confluenceLink = getPrimaryConfluenceLink()
def writer = new StringWriter()
def xml = new MarkupBuilder(writer)
assert confluenceLink // must have a working app link set up
CustomFieldManager customFieldManager = ComponentAccessor.customFieldManager
def issueManager = ComponentAccessor.getIssueManager()
def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser)
def searchProvider = ComponentAccessor.getComponent(SearchProvider)
def searchService = ComponentAccessor.getComponent(SearchService)
def user = ComponentAccessor.jiraAuthenticationContext.getLoggedInUser()
HashSet batchset = new HashSet();
def impcomponent = customFieldManager.getCustomFieldObjectByName("Impacted Technical Components")
def impcomponentvalues = issue.getCustomFieldValue(impcomponent)
log.info(impcomponentvalues)
impcomponentvalues.each { impcomponentvalue ->
Issue issue = impcomponentvalue as MutableIssue
if(issue.getIssueType().name == "Component") {
def typeofcomponent = impcomponentvalue.toString()
MutableIssue childissue = ComponentAccessor.getIssueManager().getIssueObject(typeofcomponent)
def cField = customFieldManager.getCustomFieldObjectByName("Type of Component")
def val = childissue.getCustomFieldValue(cField).toString()
typecomponentset.add(val)
}
else if(issue.getIssueType().name == "Flow") {
def typeofcomponent = impcomponentvalue.toString()
MutableIssue childissue = ComponentAccessor.getIssueManager().getIssueObject(typeofcomponent)
def cField = customFieldManager.getCustomFieldObjectByName("Type of Flow")
def val = childissue.getCustomFieldValue(cField).toString()
typeflowset.add(val)
}
else {
log.info("other technical component")
}
}
log.info(typecomponentset)
def query = jqlQueryParser.parseQuery("project = DD AND \"Type of Impacted Technical Component\" = Batch")
def search = searchService.search(user, query, PagerFilter.getUnlimitedFilter())
search.getIssues().each { documentIssue ->
def dod = issueManager.getIssueObject(documentIssue.id)
log.info(dod.summary)
batchset.add(dod.summary.toString())
}
def query1 = jqlQueryParser.parseQuery("project = DD AND \"Type of Impacted Technical Component\" = Interface")
def search1 = searchService.search(user, query1, PagerFilter.getUnlimitedFilter())
search1.getIssues().each { documentIssue1 ->
def dod1 = issueManager.getIssueObject(documentIssue1.id)
log.info(dod1.summary)
batchset.add(dod1.summary.toString())
}

int count = 0;
batchset.each { e ->
xml."ac:task-list"{
"ac:task"{
"ac:task-id"(count)
"ac:task-status"("incomplete")
"ac:task-body"(e.toString())
}
}
}

def params = [
title: "${issue.key}" + " : " + "${issue.summary}",    //page title
tamplateid : 12386309 , // template id
key : issue.key ,          //issue key
body : writer.toString()     // batch Dod's
]


def authenticatedRequestFactory = confluenceLink.createImpersonatingAuthenticatedRequestFactory()
authenticatedRequestFactory
.createRequest(Request.MethodType.POST, "/rest/scriptrunner/latest/custom/createPageFromTitle")
.addHeader("Content-Type", "application/json")
.setRequestBody(new JsonBuilder(params).toString())
.execute(new ResponseHandler<Response>() {
@Override
void handle(Response response) throws ResponseException {
if(response.statusCode != HttpURLConnection.HTTP_OK) {
throw new Exception(response.getResponseBodyAsString())
}
else {
def webUrl = new JsonSlurper().parseText(response.responseBodyAsString)["_links"]["webui"]
}
}
})

In the above code ,I am passing 4 paramters:

1.page title

2.page template id

3.issue key

4. set of values that needs to be appear in the confluence page as checklist values (which is shown in the below screenshot)

button1.PNG

To receive the above parameters I created the REST endpoint in confluence using the below script.

import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.transform.BaseScript
import org.codehaus.jackson.map.ObjectMapper
import com.atlassian.confluence.spaces.Space
import com.atlassian.confluence.pages.Page
import com.atlassian.confluence.spaces.SpaceManager
import com.atlassian.confluence.pages.PageManager
import com.atlassian.confluence.core.DefaultSaveContext
import com.atlassian.confluence.api.impl.sal.ConfluenceApplicationProperties
import javax.ws.rs.core.Response
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.confluence.pages.templates.PageTemplateManager

/*import com.atlassian.confluence.pages.templates.variables.Variable
import com.atlassian.confluence.pages.templates.PageTemplate*/

import javax.servlet.http.HttpServletRequest
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response
import static com.atlassian.user.security.password.Credential.unencrypted


@BaseScript CustomEndpointDelegate delegate

import org.apache.log4j.Logger
import org.apache.log4j.Level
def log = Logger.getLogger("com.acme.workflows")
log.setLevel(Level.DEBUG)

def confluenceApplicationProperties = ComponentLocator.getComponent(ConfluenceApplicationProperties)
createPageFromTitle(
httpMethod: "POST") {MultivaluedMap queryParams, String body ->

def mapper = new ObjectMapper()
def params = mapper.readValue(body, Map)
assert params.key
assert params.title
assert params.tamplateid
assert params.body

//log.info(params.dod.getClass().getName())
def pageTemplateManager = ComponentLocator.getComponent(PageTemplateManager)
def spaceManager = ComponentLocator.getComponent(SpaceManager)
def pageManager = ComponentLocator.getComponent(PageManager)
Space space = spaceManager.getSpace("TES")
Page HomePage = space.getHomePage()
def NewParentPage
try {
def pageTemplate = pageTemplateManager.getPageTemplate(params.tamplateid as Long)
String tamplateAsString = pageTemplate.getContent()
String issuekey = params.key
String pageTitle= params.title
NewParentPage = new Page(title: pageTitle, bodyAsString: tamplateAsString, space: space, parentPage: HomePage)
NewParentPage.setBodyAsString(tamplateAsString.replaceAll("batchset","${params.body}")) //here I am setting the body of the confluence page
pageManager.saveContentEntity(NewParentPage, DefaultSaveContext.DEFAULT)
HomePage.addChild(NewParentPage)
pageManager.saveContentEntity(HomePage, DefaultSaveContext.MINOR_EDIT)
} catch (e) {
return Response.serverError().entity([error: e.message]).build()
}

Response.ok().build()
}

My template format is as shown below

template structure.PNG

here what i am doing is I am replacing the "batchset" value with the "body" which is received from Jira.

Using the above code I am successfully able create a page in confluence.

But my concern is ,

If someone adds a new values to the batchset in jira I need to update the page with the newly added values.

So to update the page , I created a button on the confluence page. "My requirement is on clicking the "Refresh" button I want to update the existing confluence page with the newly added values ".

I am not getting any way to update the page" .

Do you have any idea to achieve it ?"

Please help me to resolve this bottleneck.

Thanks in advance 

Kind Regards

Sushma

Like Daniel Volinski likes this
Sushma September 22, 2019

Hi @Jonny Carter 

I am using script macro to create a button in confluence page.

On clicking the button it should call the REST api of jira server to update the existing confluence page with the new values .

script macro.PNG

Kind regards

Sushma

Sushma September 26, 2019

Hi @Jonny Carter 

Any workaround or suggestion ?

Regards

Sushma

Jonny Carter
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
October 1, 2019

That helps a lot, @Sushma, thanks! This is an interesting use case.

So, you could potentially do it as you are, using a scripted macro that adds a button to the page... when you click it, it refreshes.

A better idea, I think, would be one of two paths:

1. Create a script macro that simply pulls & renders the information you need from Jira on the fly. It's always up to date that way. The downside is, no one can make edits to that content, as they can with your current solution. Depending on your use case, that might be a benefit, though.

2. Create a listener in Jira that, on the IssueUpdated event, calls a separate REST endpoint in Confluence that updates the page, rather than creating a new one.

The second path will probably be the most straightforward to implement with your current setup. All you'd really need to change would be writing a REST endpoint in Confluence that updated the page, rather than creating one. That's pretty straightforward. You use the same PageManager.saveContentEntity method you used to create the page, you just need to pass it the page you want to update, rather than a new page object. Since you have a standard format for your page title, you could simply call pageManager.getPage("SPACEKEY", "Page title").

In fact, you could probably bypass writing the Listener in Jira & the REST Endpoint in ScriptRunner for Confluence entirely, and just create a Remote Event Listener in Confluence that listens for the linked Jira instance's Issue Updated event. That said, it might be quickest for you to start with the familiar tool of the REST endpoint.

Anyway, that's the rough outline. If you have some more questions about the details, feel free to ask. It would help to know a bit more about the why of this use case. I understand a comment thread can get a bit crowded, so if you need to start a new question where you lay out your use case from first principles, that's fine. You can @mention me. :)

Sushma October 2, 2019

Hi @Jonny Carter 

Thanks for your valuable suggestion and support.

Regards

Sushma

Like Jonny Carter likes this

Suggest an answer

Log in or Sign up to answer
TAGS
AUG Leaders

Atlassian Community Events