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).
Community moderators have prevented the ability to post new answers.
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.
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");
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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:
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
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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)
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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) } }
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Hi Peter,
Can you share the complete script?
Regards,
Rafael
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Community moderators have prevented the ability to post new answers.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.