Once upon a time, things were simple.  If you wanted to retrieve a page using the Confluence Java API, you simply called getPage().    Fetching Spaces was similarly easy, and intuitive.

Those days are over.  The methods are deprecated. Instead, we now need to use SpaceService and ContentService to manage spaces and content, respectively.  Let’s take a look at some examples of how a task would have been accomplished with the PageManager and SpaceManager, and compare that to how those tasks would be accomplished today.

This the code required to return all page content for all spaces, using PageManager and Spacemanager:
 import com.atlassian.confluence.pages.PageManager
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.confluence.spaces.SpaceManager

def spaceManager = ComponentLocator.getComponent(SpaceManager)
def pageManager = ComponentLocator.getComponent(PageManager)

def spaces = spaceManager.getAllSpaces()

spaces.each { space ->
    def pagesInSpace = pageManager.getPages(space, true)
    
    pagesInSpace.each { page ->
        log.warn(page.getBodyAsString())
    }
}
 

 

Here’s the same code, using the SpaceService and ContentService classes:

 import com.atlassian.confluence.api.model.Expansions
import com.atlassian.confluence.api.model.content.ContentRepresentation
import com.atlassian.confluence.api.model.content.ContentBody
import com.atlassian.confluence.api.model.content.Content
import com.atlassian.confluence.api.model.content.Space
import com.atlassian.confluence.api.model.Expansion
import com.atlassian.confluence.api.model.pagination.PageResponse
import com.atlassian.confluence.api.service.content.SpaceService
import com.atlassian.confluence.api.service.content.ContentService
import com.atlassian.confluence.api.model.content.ContentType
import com.onresolve.scriptrunner.runner.ScriptRunnerImpl
import com.atlassian.confluence.api.model.pagination.SimplePageRequest


def contentService = ScriptRunnerImpl.getPluginComponent(ContentService)
def spaceService = ScriptRunnerImpl.getPluginComponent(SpaceService)
 
SimplePageRequest pageRequest = new SimplePageRequest(0, 10)
 
PageResponse < Space > spaceResults = spaceService.find(new Expansion('name')).fetchMany(new SimplePageRequest(0, 10))
 
List < Space > 

 

This script fetches all of the projects in a Jira Cloud instance. It then fetches all of the project roles for that project, and finally fetches all of the users in that role for that project. In this way, it iterates through the projects and returns information about the users in the project roles.

 

 import groovy.json.JsonSlurper

def sb = []
//Define a string buffer to hold the results

def getUsers = get("/rest/api/2/project")
  .header('Content-Type', 'application/json')
  .asJson()
//Get the list of projects in the instance

def content = getUsers.properties.rawBody
//Get the raw body contents of the HTTP response

def scanner = new java.util.Scanner(content).useDelimiter("\\A")
String rawBody = scanner.hasNext() ? scanner.next() : ""
def json = new JsonSlurper().parseText(rawBody)
//Turn the raw body contents into JSON

json.each{ project ->
//Iterate through the projects
  
  sb.add("$project.name")

  def getRoles = get("/rest/api/2/project/$project.id/role")
    .header('Content-Type', 'application/json')
    .asObject(Map)
//For each project, get the list of roles


  getRoles.body.each{ projectRole ->
  //Iterate through the project roles

      def getRoleMembers = get("$projectRole.value")
      .header('Content-Type', 'application/json')
      .asObject(Map)
      //Return the details about each role

    getRoleMembers.body.actors.each{ roleMember ->
    //Get all the actors (users) in that role

        sb.add("$getRoleMembers.body.name:   $roleMember.displayName")
    }
  }
}

return sb
//Return the results


 

Mitigating CORS Errors With Custom Jira REST API Endpoints

If you dive into the world of REST requests and APIs, you may encounter a CORS error that prevents your request from completing. CORS stands for Cross-Origin Resource Sharing.  Same-origin is a security feature in browsers that prevents requests coming from one place (origin) to access resources in a different domain.  CORS allows web pages to access resources on a different network by providing a standard for safely allowing cross-origin requests.

Let’s talk about the example that I encountered.  I wrote a JavaScript macro for Confluence Server, and I was trying to access a third-party API using that macro.  However, Confluence macros run in the browser when the page loads, rather than running on the back-end Confluence server itself.   Thus, while the Confluence server may be set up to address CORS, your browser almost certainly is not, and the request gets blocked.

We can address this by creating a custom REST API endpoint in Confluence (or Jira).   In this way, we have the server making the request to the third party API, and the macro makes the request to the internal API.

In other words, the custom REST API endpoint acts

The amount of code required to fetch information from Confluence Cloud and bring it into Jira Cloud is a bit shocking. In a good way.

Here’s the code:

 import org.jsoup.*

def authString = "<authstring>"

def fieldConfigsResult = get("https://<url>.atlassian.net/wiki/rest/api/content/229377?expand=body.storage")
  .header('Content-Type', 'application/json')
  .header("Authorization", "Basic ${authString}")
  .asObject(Map)

def storage = fieldConfigsResult.body.body.storage.value


return storage
 

 

In the end it’s all just REST.  So long as you can authenticate, UNIREST allows us to pretty easily fetch information from other sites.

If you’d like to learn more about authenticating against Jira Cloud, check out my post on the subject.

This script takes a list of custom field names, and searches each issue in the instance for places where that custom field has been used (i.e., where it has a value other than null).

In this way, we gain insight into the usage of custom fields within a Jira Cloud instance.

The script is interesting for a number of reasons. First, it’s another instance of us having to parse a rawBody response before we can make use of it. Second, it handles the need for pagination, which we’ve also talked about in recent posts.

My intent for this script is for it to serve as a basis for future custom field work in Jira Cloud. Namely, I’d like to be able to easily rename any field with “migrated” in the title.

 

 import groovy.json.JsonSlurper
 
def stillPaginating = true
//Loop to paginate
 
def startAt = 0
//Start at a pagination value of 0
 
def maxResults = 50
//Increment pagination by 50
 
def issueIDs = []
def fieldNames = ["Actual end", "Actual start", "Change risk", "Epic Status"]
def customFieldMap = [: ]
 
 
//Get all the fields in the system
def fieldIDs = get("/rest/api/3/field")
  .header('Content-Type', 'application/json')
  .header('Accept', 'application/json')
  .asBinary()
 
def inputStream = fieldIDs.rawBody

Get All Filters in a Jira System

Here’s the truth: getting all of the filters in a Jira DC instance with ScriptRunner is awkward and fussy.  There’s no method that simply returns all of the filters. 

Instead, we need to first return all of the users in the system.  And then we need to examine all of the filters that each of them owns, as each filter must have an owner.  Here’s an example of some code that does that:

 import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.bc.filter.SearchRequestService
import com.atlassian.jira.issue.search.SearchRequest
import com.atlassian.jira.bc.user.search.UserSearchParams
import com.atlassian.jira.bc.user.search.UserSearchService
   
   
SearchRequestService searchRequestService = ComponentAccessor.getComponent(SearchRequestService.class)
UserSearchService userSearchService = ComponentAccessor.getComponent(UserSearchService)
def sb = new StringBuffer()
   
UserSearchParams userSearchParams = new UserSearchParams.Builder()
    .allowEmptyQuery(true)
    .includeInactive(false)
    .ignorePermissionCheck(true)
    .build()
//Define the parameters of the query
  
//Iterate over each user's filters
userSearchService.findUsers("", userSearchParams).each{ApplicationUser filter_owner ->
    try {
        searchRequestService.getOwnedFilters(filter_owner).each{SearchRequest filter->
            String jql = filter.getQuery().toString()
            //for each fiilter, get JQL and check if it contains our string
             
                sb.append("Found: ${filter.name}, ${jql}\n" + "<br>")
             
        }
    } catch (Exception e) {
            //if filter is private
           sb.append("Unable to get filters for ${filter_owner.displayName} due to ${e}")
    }
}
 
return sb 

 

Getting a list of filters on Jira Cloud is much simpler, as there’s a REST API that accomplishes this.  If we

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 

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