Confluence Meta-Macros and the joy of being organized

Let’s talk about meta-macros.  That is, macros that examine other macros.   I just made up the term, so don’t be concerned if you can’t find other examples on the internet.

If you wanted some insight into which pages in your Confluence Instance were using a specific macro, how would you find that information?

You could certainly check each page manually, but that sounds dreadful.

One option to get Macro information is this ScriptRunner script that I wrote, which examines the latest version of each page in each Space for references to the specified macro:

 import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.confluence.spaces.SpaceManager
import com.atlassian.confluence.pages.PageManager
import com.atlassian.confluence.pages.Page
def pageManager = ComponentLocator.getComponent(PageManager)
def spaceManager = ComponentLocator.getComponent(SpaceManager)
def spaces = spaceManager.getAllSpaces()
  
def macroRef = 'ac:name="info"'
  
spaces.each {
  spaceObj ->
    def pages = pageManager.getPages(spaceObj, false)
  pages.each {
    page ->
      if (page.getBodyContent().properties.toString().contains(macroRef) && page.version == page.latestVersion.version) {
        log.warn("'${page.title}' (version ${page.version}) is the latest version of the page, and contains the target macro")
      }
  }
}
 

 

But what if you wanted MORE information?  What if you wanted to know every macro running on every page in the system, and you didn’t have ScriptRunner to do it for you?  In that

Overview

I spoke to someone recently on the subject of learning to use ScriptRunner and Groovy.  One of the questions he had was around the number of results that were returned when he called the API.  That is, he had set maxResults to 500, but only 100 results were returned.   Why?
It’s true that you have some agency over the number of results that are returned by the Jira (and Confluence) REST API.  You can set the maxResults value as part of the request, and receive more than the 50 items that are returned by default.   However, there are limits to this parameter.  The API has a built-in limit, and no matter what you set maxResults to, you cannot exceed that limit.

Let’s look at an example.

If I call /rest/api/3/search?jql=project is not EMPTY, it’ll return every issue in the instance from a project that isn’t empty.  That’s potentially a lot of issues. 
However if we look at the results that are returned, there are some meta attributes in addition to the issues themselves.  In our example, the JSON that is returned starts with this:
 {
"expand": "schema,names",
"startAt": 0,
"maxResults": 50,
"total": 35,
 
From this, we see that

This script searches for Jira filters by name, then by filter ID, and then updates the permissions set applied to them.

It’s useful for updating a bulk list of filter names, but really it’s an exercise in working with filters and the search service.

The script iterates through an array of filter names. For each filter name, it uses the searchRequestManager to find that filter object.  It then uses the filter ID of the object to return a filter object.  This seems redundant, but the object returned by the searchRequestManager isn’t actually a filter. It’s information about the filter. 

When we search for the filter by name using searchRequestManager , it will return any filter that has that name. For that reason, we must treat the results like an array, and iterate through them.  For each filter ID that is returned by the name search, we use the searchRequestManager to search for the filter object.

Once we’ve got a filter object, we apply the new permissions to it and commit the change with updateFilter().

 

 

 import com.atlassian.jira.bc.JiraServiceContextImpl
import com.atlassian.jira.bc.filter.SearchRequestService
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.sharing.SharePermissionImpl
import com.atlassian.jira.sharing.SharedEntity
import com.atlassian.jira.sharing.type.ShareType
import com.atlassian.jira.issue.search.*

def searchRequestService = ComponentAccessor.getComponent(SearchRequestService)
def currentUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser
def searchRequestManager 

Introduction

Well, it finally happened.  I finally had to start learning JavaScript.

It’s actually not that bad, I probably should have learned a while ago.  My use case for it is writing Confluence Macros and plugins for both Confluence and Jira. I started with the plugins, for simplicity’s sake.  
My inspiration came from a post on the Atlassian Community Forums. Someone had requested a way to essentially mirror the setup of a macro. But they wanted to mirror the most recent child page, of a parent page.
I think that without pretty strong knowledge of Confluence and the REST API, I’d have struggled to complete this.  It enough work to learn JavaScript’s basic tenets as I went.

 

Digging Into The Problem

Okay so what do we actually need the script to do? We need it to:

  •  Figure out the most recently updated child page of a parent page
  •  Fetch the macro setup of the child page
  • Update the parent page accordingly

These are the three high-level functions that the macro needs to accomplish. 

Figuring out the most recently updated child page wasn’t hard.  You can make a call to baseURL + pageID + “/child/page?limit=1000&expand=history.lastUpdated. This returns a list

As is often the case, the point of this blog isn’t so much to explain how to do something complicated. The point is that I’m trying to explain something simple that should be easy to find an answer for, but was not. In this case my question was “what on earth is a UserTemplate (User Template) in Confluence”?

On the surface, it seems like creating a new user in Confluence should be a pretty straightforward process.   There’s a UserAccessor class, and that class has a createUser() method.   However, the expected inputs to that method are a User Template object and a Credentials object. From the class documentation:

  • createUser

     ConfluenceUser createUser(com.atlassian.user.User userTemplate,
                              com.atlassian.user.security.password.Credential password)

The import required to work with Credential is spelled out for us, but the userTemplate is a different story.   There’s virtually no documentation on what that means, and no amount of Googling “Confluence User Template”, “UserTemplate”, “Confluence Create User Template” will actually tell you what to do. Part of the issue is that “template” means several different things in the context of Confluence, so that muddies the waters.

Let’s cut to the chase. Here’s the code that I eventually came up with:

 import com.atlassian.confluence.api.model.people.User
import com.atlassian.sal.api.component.ComponentLocator
import 

Introduction

In a bid to contribute more to the Atlassian Community, I took a look at the most recent requests and questions on the Forums.  One that caught my eye was a request for a Confluence Macro that would:

“…display on the restricted page exactly who has access (including a breakout of all group members, not just the group name) to create transparency and build confidence in the selected user group that they are posting in the appropriately restricted area.”

I’d never created a Confluence Macro before, and this seemed like a challenge I could meet.

Please note that this isn’t a how-to on creating Macros, but really just an accounting of my experience learning the tool.

Getting Started

The first thing I did was check to see what Atlassian has to say on the subject. Confluence Macros are written in Apache Velocity, which is quite different from the Groovy that I’m used to working with.

All of the functional lines in Velocity start with a #, which makes a Velocity script look like one big page of commented-out code.  The truth is that Velocity is very old and pretty clunky.  The last news update to the project was

This script returns a list of pages in a Confluence instance that use a specified macro.

There are a number of references to “MacroManager” in the Confluence API documentation, but none of the implementations seemed to work for me.

For that reason, our best bet for checking on Macro usage is to examine the body content of each page, and look for a specific reference to the macro in question.

We also need to check that the page in question is the latest version of the page. Otherwise the script checks all versions of all pages.

 

 import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.confluence.spaces.SpaceManager
import com.atlassian.confluence.pages.PageManager
import com.atlassian.confluence.pages.Page
def pageManager = ComponentLocator.getComponent(PageManager)
def spaceManager = ComponentLocator.getComponent(SpaceManager)
def spaces = spaceManager.getAllSpaces()
  
def macroRef = 'ac:name="info"'
//Replace "info" with the name of the macro you want to assess
  
spaces.each{  spaceObj ->
//Get all Spaces in the instance
  def pages = pageManager.getPages(spaceObj, false)
  pages.each{    page ->
//Get all pages in the instances
      if (page.getBodyContent().properties.toString().contains(macroRef) && page.version == page.latestVersion.version) {
      //Check if the page contains the macro, then check to see if it's the most current version of the page
        log.warn("'${page.title}' (version ${page.version}) is the latest version of the page, and contains the target macro")
      }
  
There’s nothing terribly complicated about what I’m posting today, but it’s been a while since I put out an update.
The challenge today was to create a sort of interlocking series of issues, using a listener. That is, the listener listens for an issue to be created. It creates a bug and an epic within the same project. It then sets the epic link of the original issue and bug to be the newly created epic. It was also an exercise in balancing efficiency with readability.  Certainly, there are much more efficient ways this script could have been written. But I think as an example, it’s fairly readable, and that has value too.   
 //Example Listener for Jira Cloud - Create Bug and Epic Automatically
 
//Author: Ken McClean / kmcclean@daptavist.com
 
//Script functions:
// - Script runs as a listener
// - On issue create, script creates a new epic
// - It also creates a new bug
// - The script then sets the epic link of the original issue and the new bug to be the newly created epic
 
//References:
//https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-post
//https://library.adaptavist.com/entity/create-subtasks-when-issue-created
 
//BEGIN DECLARATIONS:
final bugTypeID = "10008"
final epicTypeID = "10000"
final epicNameField = "customfield_10011"
final epicLinkField 

One of the challenges that Jira admins face is monitoring the health of their Jira instance. While there are some built-in tools for doing this, it’s useful to know how to perform routine maintenance using ScriptRunner.
One such routine maintenance task is the monitoring of Jira Project sizes.   There’s no direct method or way of doing this; instead we get all of the issues for a given project, and sum up the size of the attachments on each issue.  In this way, we get an idea of which Jira Projects are becoming unwieldy.

The code required to perform this calculation isn’t complicated. However, if the script runs too long it’ll simply time out. This is especially true if you’re calculating the size of multiple Projects.  Here’s the code to calculate the size of a single project:

 import org.ofbiz.core.entity.GenericValue;
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.Issue;
import com.atlassian.jira.issue.IssueManager;
import com.atlassian.jira.project.Project;
import com.atlassian.jira.project.ProjectManager

def totalSize = 0
def attachmentManager = ComponentAccessor.getAttachmentManager()

ProjectManager projectManager = ComponentAccessor.getProjectManager()

def projectName = "<projectName>"

Project proj = projectManager.getProjectByCurrentKey(projectName)

IssueManager issueManager = ComponentAccessor.getIssueManager()

for (GenericValue issueValue: issueManager.getProjectIssues(proj.genericValue)) {
    Issue issue = issueManager.getIssueObject(issueValue.id)
    attachmentManager.getAttachments(issue).each {
      attachment ->
        totalSize += attachment.filesize
    }
  }
  log.warn("Total size of attachments for ${proj.name} is ${totalSize / 1024} 

Here’s a relatively simple one for you. Say you had a Jira Cloud instance with hundreds or thousands of projects, and you wanted to swap them all over to using a new notification scheme.  How would you go about it?

Well you could certainly do it by hand. That’s an option.  Or you could write a little script and us an API endpoint that I’ve only just discovered.

The script below fetches all of the projects in the system that use a notification scheme. We then filter out only the ones we want to adjust, and update each of those projects to use the target notification scheme.

This script is a great example of the kind of thing that ScriptRunner excels at, and it’s a great script to use to start learning the REST API.

 

 import groovy.json.JsonSlurper
 
// Notification scheme ID to search for
def searchSchemeId = "10000"
 
// Notification scheme ID to use for update
def updateSchemeId = 10002
 
def response = get("/rest/api/3/notificationscheme/project")
  .header('Content-Type', 'application/json')
  .asJson()
 
// Parse the JSON response
def jsonSlurper = new JsonSlurper()
def json = jsonSlurper.parseText(response.getBody().toString())
 
// Access the nodes in the JSON
json.values.each {
  project ->
 
    logger.warn(project.toString())
 
//We need to account for -1,