Create
cancel
Showing results for 
Search instead for 
Did you mean: 
Sign up Log in

Set page author after ScriptRunner CopyTree

Peter-Dave Sheehan
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 2, 2017

I've created a custom rest API that calls the canned script CopyTree (called from a web item dialog using ajax in a js web resource).

This works fine thanks to many other resources I found here including: https://answers.atlassian.com/questions/39582646

But I'd like the pages in the tree to be authored by the current user.

I'm able to get the current user and I'm able to get the ID of first ancestor of the new page tree.

So I'm trying to call a function I created like this:

def setPageThreeAuthor(pageId,authorUserName){
   ChangeContentAuthor changeContentAuthor = new ChangeContentAuthor()
   def inputs=[
      FIELD_CQL_CLAUSE:"id=${pageId} or ancestor = ${pageId}",
      FIELD_NEW_AUTHOR_NAME:"${authorUserName}"
    ]
    log.debug(inputs)
    def errorCollection = (SimpleBuiltinScriptErrors)	changeContentAuthor.doValidate(inputs, false)
    if(errorCollection.hasAnyErrors()) {
      log.warn("Could not set copied page tree author to current user: $errorCollection")
    } else {
      def output = changeContentAuthor.doScript(inputs);
      log.debug(output)
    }
}

But I'm always getting:

2017-03-02 20:06:37,054 DEBUG [http-nio-8090-exec-3] [onresolve.scriptrunner.runner.ScriptRunnerImpl] call {FIELD_CQL_CLAUSE=id=113836669 or ancestor = 113836669, FIELD_NEW_AUTHOR_NAME=p6s}
2017-03-02 20:06:37,084 WARN [http-nio-8090-exec-3] [onresolve.scriptrunner.runner.ScriptRunnerImpl] call Could not set copied page tree author to current user: Errors: [FIELD_CQL_CLAUSE:The CQL clause must return at least one result to rename author on]

When I use the CQL in the web form for the built-in script, I'm getting the expected number of page in my new tree.

Any thoughts? 

I'm thinking either the CQL/inputs are not being passed properly, or the CQL actually can't find the page because the search index takes a bit of time (probably not though, since adding a sleep statement doesn't fix it).

4 answers

1 accepted

Comments for this post are closed

Community moderators have prevented the ability to post new answers.

Post a new question

0 votes
Answer accepted
Peter-Dave Sheehan
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 6, 2017

I think I was right in my initial instinct... it's an indexing issue.

After calling the CopyTree().doScript(), the changes are added to the IndexTaskQueue

Then when I attempted to call ChangeContentAuthor().doScript() I was getting an error when evaluating the CQL since the index has not yet been flushed.

So, using com.atlassian.confluence.search.IndexManager and the flushQueue() method, I'm partially able to get around it:

...
def output = copyTree.doScript(inputs)
def newPageTreeId = pageManager.getPage(targetSpace,sourcePageTitle.replaceAll(bodyObject.replaceToken, bodyObject.replaceValue)).id
ComponentLocator.getComponent(IndexManager).flushQueue())
setPageTreeAuthor(newPageTreeId,AuthenticatedUserThreadLocal.get().name)

The pages are re-assigned to the new content author some times... but not always. 

A little bit more digging and I found that the queued tasks are not always immediately available. There must be some asynchronous threading happening somewhere.

It's a little crude, but I can get around it by flushing the queue just before the copyTree.doScript just to clear any items from regular usage. Then wait for the taskQueue to contain new items, then flush them.

...
def indexManager = ComponentLocator.getComponent(IndexManager)
indexManager.flushQueue()
def output = copyTree.doScript(inputs)
def newPageTreeId = pageManager.getPage(targetSpace,sourcePageTitle.replaceAll(bodyObject.replaceToken, bodyObject.replaceValue)).id
def waitTime =0
def waitTime =5000
while (indexManager.taskQueue.size == 0 && waitTime < waitLimit) {
	sleep(100)
	waitTime = waitTime+100
}
setPageTreeAuthor(newPageTreeId,AuthenticatedUserThreadLocal.get().name)

Doing this in an environment in dev mode, with some debug messaged, I've gone in the while loop anything from 0 to 5 times.

Another slightly better option is to look for the newPageId in the indexQueue and wait for it to appear. This would be better than flushing before the copyTree and hoping nothing else populates the indexQueue before I call the setPageTreeAuthor:

while (waitTime < waitLimit && 
	!indexManager.getTaskQueue().getQueuedEntries().any{
		it.handle.toString().contains(newPageTreeId.toString())
	} ) {
    sleep(100)
    waitTime = waitTime + 100
}

 

I'm still not happy about the while loop with sleep.

I'm open to suggestions if anyone has a better idea. 

Rafael Franco
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.
March 6, 2017

Good finding. You could try to implement a mechanism to wait for the reindexing to finish. Something like:

 

private static final Function<ConfluenceIndexManager, Boolean> flushOrReIndexCheck = new Function<ConfluenceIndexManager, Boolean>() {
    public Boolean apply(ConfluenceIndexManager indexManager) {
        return indexManager.isFlushing() || indexManager.isReIndexing();
    }
};

Function comes from guava, so should be available.

Then something like:

protected <F> boolean poll(Function<F, Boolean> check, F input, long timeout) {
    long timeoutAfter = System.currentTimeMillis() + timeout;
    while (!check.apply(input)) {
        if (System.currentTimeMillis() > timeoutAfter) {
            return false;
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            // Ignore
        }
    }
    return true;
}
if (!poll(flushOrReIndexCheck, indexManager, timeout)) {
    log.error("Timed out waiting for indexing task");
    return timeout();
}

log.info("Indexing task complete");

 

 

4 votes
Rafael Franco
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.
March 6, 2017

Hi Peter,

I've looked at your script and mostly it is correct. For setPageTreeAuthor you'll need to pass the pageId of the newly created tree parent page, so simply:

//get the Id of the top level of the new page tree
setPageThreeAuthor(bodyObject.targetParentPage.id,AuthenticatedUserThreadLocal.get().name)

Then regarding the CQL you'll need only the ancestor part:

def setPageThreeAuthor(pageId,authorUserName){
   //Define the inputs for the ChangeContentAuthor canned script
   def inputs=[
      FIELD_CQL_CLAUSE:"ancestor = ${pageId}",
      FIELD_NEW_AUTHOR_NAME:"${authorUserName}"
   ]
   log.debug(inputs)
   //Create an instance of the ChangeAuthor canned script class and run the validation
   ChangeContentAuthor changeContentAuthor = new ChangeContentAuthor()
   def errorCollection = (SimpleBuiltinScriptErrors) changeContentAuthor.doValidate(inputs, false)
   if(errorCollection.hasAnyErrors()) {
      log.warn("Could not set copied page tree author to current user: $errorCollection")
   } else {
      //Run the actual script and store the input
      def output = changeContentAuthor.doScript(inputs);
      log.debug(output)
   }
}

 

As it's mentioned in the documentation:

https://developer.atlassian.com/confdev/confluence-server-rest-api/advanced-searching-using-cql/cql-field-reference#CQLFieldReference-ancestorAncestorAncestor

Search for all pages that are descendants of a given ancestor page. This includes direct child pages and their descendents. It is more general than the parent field.

 

Let me know if this worked for your.

Regards,

Rafael

Peter-Dave Sheehan
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 6, 2017

Thanks for the input Rafael

But in my current case, the new parent where the new pages are created may already have other children.

That's why I was not giving setPageTreeAuthor that page id (bodyObject.targetParentPage.id), but instead, I tried to identify the top of the tree that was just copied with: 

newPageTreeId = pageManager.getPage(targetSpace,sourcePageTitle.replaceAll(bodyObject.replaceToken, bodyObject.replaceValue)).id

This should apply the same title transformation for the top of the source page tree, and search for the corresponding page in the target's children.

And that is also why in my CQL i included id=${pageId} with the ancestor. Since newPageTreeid is only one of the children of targetParentPage, and I want that child and all if its descendants. 

I ran some more tests, and my function works fine as a standalone when i run in in the console. So the problem seems to be that  I'm not getting any CQL results so soon after the page tree has been copied. Are there any utils to make sure the page is finished being added to the index before I call the setPageTreeAuthor? (just realized my earlier spelling of three instead of tree)

1 vote
Peter-Dave Sheehan
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 3, 2017

Here is my full script. Configured as a custom rest endpoint.

import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import com.onresolve.scriptrunner.canned.confluence.admin.CopyTree
import com.onresolve.scriptrunner.canned.confluence.admin.ChangeContentAuthor
import com.onresolve.scriptrunner.canned.util.SimpleBuiltinScriptErrors
import com.atlassian.confluence.pages.AbstractPage
import com.atlassian.confluence.pages.PageManager
import com.atlassian.sal.api.component.ComponentLocator 
import com.atlassian.confluence.user.AuthenticatedUserThreadLocal
import groovy.json.JsonBuilder
import groovy.json.JsonSlurper
import groovy.json.JsonParserType
import groovy.transform.BaseScript
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response
import org.apache.log4j.Level
log.setLevel(Level.DEBUG)
@BaseScript CustomEndpointDelegate delegate
copyPageTree(
    httpMethod: "POST"
    , groups: ["confluence-users"]
	) 
	{ MultivaluedMap queryParams, String body ->
      log.debug("HttpBody= ${body}")
      JsonSlurper slurper = new JsonSlurper()
      if(!body){
         return Response.serverError().entity([error: "No parameters specified"]).build()
      } else {
         //Read and validated the post parameters
         def bodyObject = slurper.parseText(body)
         if(!bodyObject?.pageTreeSource || !bodyObject?.targetParentPage || !bodyObject?.replaceToken || !bodyObject?.replaceValue) {
             return Response.serverError().entity([error: "All parameters are require: pageTreeSource, targetParentPage, replaceToken, replaceValue"]).build()
         }
         //Setup the title transformation rule
         def titleTransform = """
         import com.atlassian.confluence.pages.AbstractPage
         titleTransform = {AbstractPage p -> p.title = p.title.replace('${bodyObject.replaceToken}', '${bodyObject.replaceValue}')}"""
         //Define the CopyTree canned script input parameters
         def inputs =[
             (CopyTree.FIELD_SRC_PAGE_ID): "[\"${bodyObject.pageTreeSource}\"]",
             (CopyTree.FIELD_TARGET_PAGE_ID): "[\"${bodyObject.targetParentPage}\"]",
             (CopyTree.FIELD_TITLE_TRANSFORM): titleTransform.toString()
         ]  
         //Create an instance of CopyTree canned script class and run the validation
         CopyTree copyTree = new CopyTree()
         def errorCollection = (SimpleBuiltinScriptErrors) copyTree.doValidate(inputs, false)
         if(errorCollection.hasAnyErrors()) {
             log.warn("Could not copy page tree: $errorCollection")
             return Response.serverError().entity([error: errorCollection.getErrorMessages()]).build();
         } else {
            //Perform the actual copdy and store the output text
            def output = copyTree.doScript(inputs)
            log.debug("copyTreeOutput1=${output}")
           
            //get the Id of the top level of the new page tree
            pageManager = ComponentLocator.getComponent(PageManager)
            targetSpace = pageManager.getPage(bodyObject.targetParentPage.toInteger()).space.key
            log.debug("spaceKey=${targetSpace}")
            sourcePageTitle = pageManager.getPage(bodyObject.pageTreeSource.toInteger()).title
            log.debug("SourcePageTitle=${sourcePageTitle}")
            log.debug("NewPageTreeParent=${sourcePageTitle.replaceAll(bodyObject.replaceToken, bodyObject.replaceValue)}")
            newPageTreeId = pageManager.getPage(targetSpace,sourcePageTitle.replaceAll(bodyObject.replaceToken, bodyObject.replaceValue)).id
            log.debug("NewPageId=${newPageTreeId}")
            //let's set the author of the new page to the current users
            setPageThreeAuthor(newPageTreeId,AuthenticatedUserThreadLocal.get().name)
            return Response.ok().entity(output).build();
         }
      }
   }
def setPageThreeAuthor(pageId,authorUserName){
   //Define the inputs for the ChangeContentAuthor canned script
   def inputs=[
      FIELD_CQL_CLAUSE:"id=${pageId} or ancestor = ${pageId}",
      FIELD_NEW_AUTHOR_NAME:"${authorUserName}"
   ]
   log.debug(inputs)
   //Create an instance of the ChangeAuthor canned script class and run the validation
   ChangeContentAuthor changeContentAuthor = new ChangeContentAuthor()
   def errorCollection = (SimpleBuiltinScriptErrors) changeContentAuthor.doValidate(inputs, false)
   if(errorCollection.hasAnyErrors()) {
      log.warn("Could not set copied page tree author to current user: $errorCollection")
   } else {
      //Run the actual script and store the input
      def output = changeContentAuthor.doScript(inputs);
      log.debug(output)
   }
}
1 vote
Rafael Franco
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.
March 2, 2017

Hi Peter,

Can you share the complete script? 

Regards,

Rafael

Comments for this post are closed

Community moderators have prevented the ability to post new answers.

Post a new question

TAGS
AUG Leaders

Atlassian Community Events