Custom Event Listener

Working with Custom Event Listeners

The script binding shares information between the script and the application. An event listener variable in the script binding is provided. This variable corresponds to the event which triggered the listener. For example, if you are listening for PageCreateEvent the event variable will be a PageCreateEvent object.

You can choose to have your handler listen for multiple events. If you need to do different things depending on the type of event, you can check that with instanceof. Alternatively, you can type the event variable to the most specific superclass. In the previous example, that would be PageEvent.

In the following example, the event is getting page content for both PageCreateEvent and PageUpdateEvent. Since PageEvent is a common superclass for both PageCreateEvent and PageUpdateEvent, you could cover both events by using the following code:

groovy
def event = event as PageEvent def content = event.content.bodyAsString

Line 1: The event is passed in the binding. This line is only used to give type information when using an IDE, and has no functional impact.

Samples

Add a Comment on Page Create

Some organizations have a particular style guide or would like to enforce specific rules. This event listener example looks at the content of new pages for banned words. If the page content contains any of a list of banned words, a comment is automatically added with an alternative suggestion. The following image is a comment generated in response to a banned word:

The code for this event listener follows:

groovy
import com.atlassian.confluence.event.events.content.page.PageEvent import com.atlassian.confluence.pages.CommentManager import com.atlassian.sal.api.component.ComponentLocator def event = event as PageEvent // use event.getPage().getSpace() if you want to restrict only to certain spaces def commentManager = ComponentLocator.getComponent(CommentManager) def body = event.content.bodyAsString def alternatives = [ "air hostess": "flight attendant", "amuck" : "amok", ] def commentBody = alternatives.findAll { badWord, goodWord -> body.contains(badWord) }.collect { badWord, goodWord -> "<li><b>${badWord}</b> should be avoided. Consider using: <b>${goodWord}</b>.</li>" }.join("") if (commentBody) { commentManager.addCommentToObject(event.content, null, "<p>Please consider the following issues: <ul>$commentBody</ul> </p>") }

In practice, you would also want to watch page updates and only look at the diff between old and new versions.

Add Inline Comment on Page Create

Similar to Add a Comment on a Page Create, you can configure your listener to add inline comments instead. As you get these comments, you can dismiss them. The following image is an example of an inline comment:

The code for this event listener follows:

groovy
import com.atlassian.confluence.event.events.content.page.PageEvent import com.atlassian.confluence.setup.settings.SettingsManager import com.atlassian.sal.api.component.ComponentLocator import groovy.json.JsonBuilder import groovyx.net.http.HTTPBuilder import groovyx.net.http.Method import net.sf.hibernate.HibernateException import net.sf.hibernate.Session import net.sf.hibernate.SessionFactory import org.apache.http.HttpRequest import org.apache.http.HttpRequestInterceptor import org.apache.http.protocol.HttpContext import org.apache.log4j.Level import org.apache.log4j.Logger import org.springframework.orm.hibernate.SessionFactoryUtils import java.sql.SQLException import static groovyx.net.http.ContentType.JSON log = Logger.getLogger("com.test.InlineScript") log.setLevel(Level.DEBUG) event = event as PageEvent def settingsManager = ComponentLocator.getComponent(SettingsManager) baseUrl = settingsManager.getGlobalSettings().getBaseUrl() def alternatives = [ "air hostess": "flight attendant", "amuck" : "amok", ] Session s = SessionFactoryUtils.getSession(ComponentLocator.getComponent(SessionFactory), false) flushAndCommitSession(s) def flushAndCommitSession(Session s) { // commit any open transaction to release any locks, as the tables get deleted // via another connection if (s != null) { try { log.info("Flushing session and committing pending transactions") s.flush() s.connection().commit() log.info("Session flush and commit complete") } catch (HibernateException he) { log.error("error flushing session", he) } catch (SQLException sqle) { log.error("error committing connection", sqle) } } } /** * find occurrence of word in a String */ Integer keyFinder(String pageBody, String key) { def keyToFind = /$key/ def keyFinder = (pageBody =~ /$keyToFind/) keyFinder.count } /** * Recursive function for creating inline comments */ def createInlineComments( Map<String, String> altMap, String pageContent, Integer matches = 0, Integer index = 0, String key = "" ) { if (matches != 0 && matches == index && key) { //loop breakout condition or reset variables when key changes altMap.remove(key) if (altMap.size() == 0) { return } def keys = altMap.keySet() as List key = keys.pop() matches = keyFinder(pageContent, key) index = 0 } else { //function entry point match count matches = keyFinder(pageContent, key) } if (altMap.size() == 0) { return } if (matches) { log.debug("Found key : ${key}") def data = [ containerId : "${event.page.getId()}", parentCommentId : 0, numMatches : matches, matchIndex : index, body : "<p>Please use <b>${altMap.get(key)}</b> instead</p>", originalSelection : key, serializedHighlights: "" ] def jsonFormattedData = new JsonBuilder(data).toString() def http = new HTTPBuilder(baseUrl) http.client.addRequestInterceptor(new HttpRequestInterceptor() { void process(HttpRequest httpRequest, HttpContext httpContext) { httpRequest.addHeader('Authorization', 'Basic ' + 'admin:admin'.bytes.encodeBase64().toString()) httpRequest.addHeader('X-Atlassian-Token', "no-check") } }) http.request(Method.POST, JSON) { uri.path = "/confluence/rest/inlinecomments/1.0/comments" requestContentType = JSON body = jsonFormattedData response.success = { resp -> log.debug("RESULT STATUS: DONE") //recursive call to the function createInlineComments(altMap, pageContent, matches, index + 1, key) } response.failure = { resp -> log.debug("FAILED : ${resp.statusLine.statusCode}") } } } else { altMap.remove(key) if (altMap.size() == 0) { return } createInlineComments(altMap, pageContent, 0, 0, (altMap.keySet() as List).pop()) } } createInlineComments(alternatives, event.content.bodyAsString, 0, 0, (alternatives.keySet() as List).pop())

Create Page When User Created

This event listener example automatically creates a user profile page in the Team space. You can use this profile page to list their skills and profile. The following image is an example of a profile page:

The event selected for this example is UserCreateEvent, and the code follows:

groovy
import com.atlassian.confluence.core.DefaultSaveContext import com.atlassian.confluence.event.events.user.UserCreateEvent import com.atlassian.confluence.pages.Page import com.atlassian.confluence.pages.PageManager import com.atlassian.confluence.spaces.SpaceManager import com.atlassian.confluence.user.ConfluenceUser import com.atlassian.sal.api.component.ComponentLocator import groovy.xml.MarkupBuilder try { def event = event as UserCreateEvent def user = event.user as ConfluenceUser def pageManager = ComponentLocator.getComponent(PageManager) def spaceManager = ComponentLocator.getComponent(SpaceManager) def teamSpace = spaceManager.getSpace("TEAM") def writer = new StringWriter() def builder = new MarkupBuilder(writer) builder.table { tbody { tr { td("About") td { "ac:link" { "ri:user"("ri:userkey": user.key) } } } tr { td("Profile") td("") } tr { td("Skillz") td("") } } } def parentPage = teamSpace.getHomePage() assert parentPage def targetPage = new Page(title: "About ${user.fullName}", bodyAsString: writer.toString(), space: teamSpace, parentPage: parentPage ) pageManager.saveContentEntity(targetPage, DefaultSaveContext.DEFAULT) parentPage.addChild(targetPage) pageManager.saveContentEntity(parentPage, DefaultSaveContext.MINOR_EDIT) } catch (anyException) { log.warn("Failed to create page for new user", anyException) }

Collecting Stats

This event listener example automatically sends statistics to statsd for page views, space views, and users/pages views. The following image is an example of the statistics for page views:

In the example, Grafana is used to visualize page views metrics.

The event selected for this example is PageViewEvent, and the code follows:

groovy
import com.atlassian.confluence.event.events.content.page.PageEvent import com.atlassian.confluence.user.AuthenticatedUserThreadLocal import groovy.transform.Field @Field final def host = "http://192.168.59.103/" @Field final def port = 8125 def event = event as PageEvent def currentUser = AuthenticatedUserThreadLocal.get() // keys to create unique nodes for counters def spaceKey = event.page.spaceKey def pageId = event.page.id as String def userKey = currentUser.name def nodeId = "confluence.stats.views" // build the unique metric keys def pageViewMetricKey = "${nodeId}.page.${pageId}" def spaceViewMetricKey = "${nodeId}.space.${spaceKey}" def userViewMetricKey = "${nodeId}.user.${userKey}.${pageId}" // increase by one the counters for the following metric keys increaseByOne(pageViewMetricKey, userViewMetricKey, spaceViewMetricKey) def increaseByOne(String... keys) { def dataToSend = "" def value = 1 //increase counter by one //syntax for counter according to https://github.com/etsy/statsd/blob/master/docs/metric_types.md for (key in keys) { dataToSend += "${key}:${value}|c\n" } def data = dataToSend.getBytes() def address = InetAddress.getByName(host as String) def packet = new DatagramPacket(data, data.length, address, port as int) def socket = new DatagramSocket() try { socket.send(packet) } finally { socket.close() } }

Creating a Jira Project Whenever a Confluence Space Is Created

This event listener example automatically creates a Jira project every time a space is created.

The event selected for this example is SpaceCreateEvent, and the code follows:

groovy
import com.atlassian.applinks.api.ApplicationLinkService import com.atlassian.applinks.api.application.jira.JiraApplicationType import com.atlassian.confluence.event.events.space.SpaceCreateEvent import com.atlassian.sal.api.component.ComponentLocator 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 static com.atlassian.sal.api.net.Request.MethodType.POST def appLinkService = ComponentLocator.getComponent(ApplicationLinkService) def appLink = appLinkService.getPrimaryApplicationLink(JiraApplicationType) def applicationLinkRequestFactory = appLink.createAuthenticatedRequestFactory() def event = event as SpaceCreateEvent def space = event.space def input = new JsonBuilder([ projectTypeKey : "business", projectTemplateKey: "com.atlassian.jira-core-project-templates:jira-core-task-management", name : space.name, key : space.key, lead : event.space.creator.name, ]).toString() def request = applicationLinkRequestFactory.createRequest(POST, "/rest/api/2/project") .addHeader("Content-Type", "application/json") .setEntity(input) request.execute(new ResponseHandler<Response>() { @Override void handle(Response response) throws ResponseException { if (response.statusCode != 201) { log.error("Creating jira project failed: ${response.responseBodyAsString}") } } })

Creating a Page When a Form Is Submitted

This event listener example automatically creates a new page when a form is submitted.

If you have Forms for Confluence installed, you can listen for the FormSubmitEvent and run a script to create a new page. You can specify the space, title, and page content of the new page.

Follow these steps to create a new page when a user submits an internal feature request or events proposal using Forms for Confluence.

  1. Create a new form that includes input fields to collect a specified pageTitle, spaceKey, and pageContent.

    You can also include additional fields, like an attachment.

  2. Configure the event listener for FormsSubmitEvent where the spaceKey, pageTitle, and pageContent corresponds to the name parameter defined for the macros in your form.

    When the form is submitted, a new page is created with the values entered by the user.

    The code for this example follows:


    groovy
    import com.atlassian.confluence.core.DefaultSaveContext import com.atlassian.confluence.pages.Page import com.atlassian.confluence.pages.PageManager import com.atlassian.confluence.spaces.SpaceManager import com.atlassian.sal.api.component.ComponentLocator import com.atlassian.xwork.FileUploadUtils import groovy.json.JsonSlurper def spaceManager = ComponentLocator.getComponent(SpaceManager) def pageManager = ComponentLocator.getComponent(PageManager) def eventFormSubmission = binding.variables.get("event") String eventData = eventFormSubmission.structuredValue // any uploaded files. ie - uploaded using the "Forms - Attachment" macro List<FileUploadUtils.UploadedFile> uploadedFiles = eventFormSubmission.uploadedFiles // Space key, page title, page content, in a map. Map<String, String[]> inputtedValues = new JsonSlurper().parseText(eventData) as Map<String, String[]> def parentPageTitle = getValue(inputtedValues, "parentPageTitle") def pageTitle = getValue(inputtedValues, "pageTitle") def spaceKey = getValue(inputtedValues, "spaceKey") def pageContent = getValue(inputtedValues, "pageContent") // construct a new page Page targetPage = constructNewPage(spaceManager, spaceKey, pageTitle, pageContent) validateSpace(spaceKey, spaceManager) validateParentPage(spaceKey, parentPageTitle, pageManager) setPageAncestryAndSave(parentPageTitle, targetPage, spaceKey, pageManager) private static String getValue(Map<String, String[]> data, String key) { if (!data.get(key) || data.get(key)[0].isEmpty()) { throw new IllegalArgumentException("A \"" + key + "\" was not provided.") } else if (hasMultipleUniqueEntries(data.get(key) as List<String>)) { throw new IllegalArgumentException("multiple \" " + key + "\"'s were provided, please enter a single \"" + key + "\"") } data.get(key)[0] } private static boolean hasMultipleUniqueEntries(List<String> entries) { Set uniqueEntries = [] as Set uniqueEntries.addAll(entries) uniqueEntries.size() != 1 } private static Page constructNewPage(SpaceManager spaceManager, String spaceKey, String pageTitle, String pageContent) { def targetPage = new Page( space: spaceManager.getSpace(spaceKey), title: pageTitle, bodyAsString: pageContent, ) targetPage } private static validateSpace(String spaceKey, SpaceManager spaceManager) { def space = spaceManager.getSpace(spaceKey) if (space == null) { throw new IllegalArgumentException("invalid space key") } } private static validateParentPage(String spaceKey, String parentPageTitle, PageManager pageManager) { def parentPage = pageManager.getPage(spaceKey, parentPageTitle) if (parentPage == null) { throw new IllegalArgumentException("invalid parentPageTitle. " + parentPageTitle + " is not found in " + spaceKey) } } private static setPageAncestryAndSave( String parentPageTitle, Page targetPage, String spaceKey, PageManager pageManager ) { Page parentPage = pageManager.getPage(spaceKey, parentPageTitle) parentPage.addChild(targetPage) targetPage.setParentPage(parentPage) pageManager.saveContentEntity(parentPage, DefaultSaveContext.DEFAULT) pageManager.saveContentEntity(targetPage, DefaultSaveContext.DEFAULT) }

Results

The following image is the resulting form:

The following image is the resulting page that is created when the form is submitted:

This event listener is not directly tied to Forms configuration. The results of the Forms configuration can be recorded and/or sent to a different destination that you choose.

On this page