Retrieve ContentEntityObject


#1

Hello,

I have a macro that runs and from the UI, invokes a REST endpoint in another class. In that target class, I’d like to retrieve the ContentEntityObject for the page since I need access to the page properties.

If I was accessing this from the macro class it would be easy since I can use the ConversionContext to get the ContentEntityObject. However, it my target class it’s not so easy. I suspect I may be able to have the class extend HttpServlet then inject the ContentEntityManager but I would still need to have the page id somehow.

Does anyone have a best practice for how to get the ContentEntityObject outside of the macro class?

Thanks!
Michael


#2

Why don’t you pass the page id you want to load to the rest and from there inject the PageManager in order to fetch the ContentEntityObject?


#3

That’s a good idea. Any idea how I can do it? I’m currently calling the REST endpoint from an action in a .vm template button.

<form id="createinitiative" class="aui" action="http://tgd-agile-4025:1990/confluence/rest/jirarequest/1.0/create/initiative" method="GET">
    <div class="field-group">
        <input type="submit" value="Create Initiative" class="button">
    </div>
</form>

#4

According to this Launch macro via button you have a macro that renders the above html which above html is a form that pokes the
http://tgd-agile-4025:1990/confluence/rest/jirarequest/1.0/create/initiative.
In the endpoint of the create/initiative you want to fetch some ContentEntityObject (i am assuming the one from which you call the rest) you mentioned in the question. Am i right?

Assuming I am right, add to your macro’s execute function just before the return:
contextMap.put("pageId",conversionContext.getEntity().getIdAsString());
In the form append the pageId as follows:
<form id="createinitiative" class="aui" action="http://tgd-agile-4025:1990/confluence/rest/jirarequest/1.0/create/initiative?pageId=$pageId" method="GET">
In your rest class now add a @QueryParam("pageId") String pageId in the create method.
You got the pageId. Inject the PageManager in the REST class and fetch the object with one of the PageManager’s methods.

Generally talking, in the REST context when you need to create something you use the POST and not GET. Grab an idea here

If I assumed right, but you still can’t make it work, post some of your code along with errors


#5

Hi Panos,

Yes, you’re absolutely right. That is what I’m trying to do. Thanks so much for the suggestions. I’ll try both (adding to the contextMap and doing a POST instead). I’ll post back here shortly. Thanks again!

Michael


#6

Hi Panos,

Taking your advice, I got the GET working but not the POST. Here’s the code:

The macro: createInitiative.java:

public class createInitiative implements Macro {
	private JsonObject pageProperties;
	public String execute(Map<String, String> parameters, String body, ConversionContext conversionContext) throws MacroExecutionException {
		
		//  populate JSON pageProperties object
		Map<String, Object> contextMap = MacroUtils.defaultVelocityContext();
		contextMap.put("pageProperties", pageProperties);
		return VelocityUtils.getRenderedTemplate("templates/createInitiative.vm", contextMap);
	}

The Velocity template: createInitiative.vm:

#requireResource("confluence.web.resources:ajs")
#requireResource("confluence.web.resources:jquery")
#requireResource("com.trustvesta.plugins:createinitiative-web-resources")

<script type="text/javascript">
function createInitiative() {

	AJS.toInit(function() {
		AJS.$.ajax({
			type: "POST",
			contentType: "application/json",
			url: AJS.contextPath() + "/rest/jirarequest/1.0/create/initiative",
			data: "$pageProperties",
			dataType: "json",
			success: function(response, textStatus) { AJS.log("Success: " + response); },
			error: function (xhr, textStatus, errorThrown) { AJS.log("Error: " + xhr); } 
		});
	});
}
</script>
<input id="createinitiative" class="aui" type="button" value="Create Initiative" onclick="createInitiative()" >

The REST endpoint: jiraRequest.java

    @Path("/initiative")
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces({MediaType.APPLICATION_JSON})
    public Response createInitiative(String pageProperties) {
       	System.out.println("In REST response with: " + pageProperties);
        return Response.ok("Success").build();
    }

The error I get is:

INFO] [talledLocalContainer] org.apache.velocity.exception.ParseErrorException: Encountered "{" at templates/createInitiative.vm[line 18, column 28]

and the button doesn’t render. Please note the line 18 is the AJS.$.ajax call. I can’t see what’s wrong with it. It strangely worked when I used “GET” but only if jQuery.get(), not AJS.$.get().

Thanks again for your help,
Michael


#7

I’ve seen this before, this pattern AJS.$.method makes the velocity engine break. This, AJS.$(“selector”).method doesn’t.
Either move the javascript inside a js file or use JQuery.ajax


#8

You also have a couple of other mistakes:

data: "$pageProperties",

will render something like data: "class.instanceobject" and not actual data. Even if you serialize the data then you get something like {&quot;id&quot;:&quot;92078504&quot;,&quot;spacekey&quot;:&quot;UADCE725&quot;} which you will have to manually deserialize. That would mean you are posting string.

I took the initiative to spare you from figuring out what is going on when rendering a json object to velocity and provide you a way to post JSON object.

The macro

package yourpackage;

import com.atlassian.confluence.content.render.xhtml.ConversionContext;

import com.atlassian.confluence.macro.Macro;
import com.atlassian.confluence.macro.MacroExecutionException;
import com.atlassian.confluence.renderer.radeox.macros.MacroUtils;
import com.atlassian.confluence.util.velocity.VelocityUtils;
import com.google.gson.Gson;
import com.google.gson.JsonObject;

import java.util.HashMap;
import java.util.Map;
public class CreateInitiative implements Macro {
    private Map<String,String> pageProperties;
    @Override
    public String execute(Map<String, String> map, String s, ConversionContext conversionContext) throws MacroExecutionException {
        Map<String, Object> contextMap = MacroUtils.defaultVelocityContext();
        pageProperties=new HashMap<>();
        pageProperties.put("id",conversionContext.getPageContext().getEntity().getIdAsString());
        pageProperties.put("spacekey", conversionContext.getSpaceKey());
        Gson gson = new Gson();

        contextMap.put("pageProperties", gson.toJson(pageProperties));
        return VelocityUtils.getRenderedTemplate("layouts/createInitiative.vm", contextMap);
    }

    @Override
    public BodyType getBodyType() {
        return BodyType.RICH_TEXT;
    }

    @Override
    public OutputType getOutputType() {
        return OutputType.BLOCK;
    }
}

The rest endpoint

@Path("/create")
public class MyRestResource {

    @Path("/initiative")
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces({MediaType.APPLICATION_JSON})
    public Response createInitiative(MyRestResourceModel id) {
        System.out.println("In REST response with: " + id.getId());
        return Response.ok(id.getId()).build();
    }
}

The json object:

import javax.xml.bind.annotation.*;
@XmlAccessorType(XmlAccessType.FIELD)
public class MyRestResourceModel {

    @XmlElement(name = "id")
    private String id;
    @XmlElement(name = "spacekey")
    private String spacekey;

    public MyRestResourceModel() {
    }

    public MyRestResourceModel(String id, String spacekey) {
        this.id = id;
        this.spacekey = spacekey;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getSpacekey() {
        return spacekey;
    }

    public void setSpacekey(String spacekey) {
        this.spacekey = spacekey;
    }
}

And finally but most importantly:

<input id="createinitiative" class="aui" type="button" value="Create Initiative" onclick="createInitiative()" >
#set($propertiesHtml=$pageProperties)
<script type="text/javascript">
    function createInitiative() {
        AJS.toInit(function() {
            jQuery0.ajax({
                type: "POST",
                contentType: "application/json",
                url: AJS.contextPath() + "/rest/myrestresource/1.0/create/initiative",
                data: '${propertiesHtml}',
                dataType: "json",
                success: function(response, textStatus) { AJS.log("Success: " + response); },
                error: function (xhr, textStatus, errorThrown) { AJS.log("Error: " + xhr); }
            });
        });
    }
</script>

Notice here that jQuery0 is my noConflict jquery. Use yours. Take a look at second line
#set($propertiesHtml=$pageProperties) this is the trick. Atlassian is escaping any variable that doesn’t end in Html.
Data line also is explaining a bit why AJS.$. is not working, velocity is marking variables with $var or with ${var}. When you have AJS.$.ajax then it expects variable. It is generally advised not to use inline javascript to avoid just that problem


#9

That did the trick! Thank you so much!


#10

I’m running into an issue here I can’t explain. As suggested by Panos at the start of the post, I inject PageManager at the REST endpoint and use that to get the Page object via the page id. From there I set a string property. This appears to work if I immediately read it back. However, another macro on the page that reads that property still shows the original value after a refresh. I’ve extracted the page id from both objects and confirmed they are the same.

Macro object gets the ContentProperties from conversionContext.getEntity().getProperties() while REST endpoint object gets if from pageManager.getPage(pageid).getEntity().getProperties().

Here’s a snippet where I set the page property:

@ComponentImport
private final PageManager pageManager;
@Autowired
public JiraRequest(PageManager pageManager) { 
    this.pageManager = pageManager;
}

@Path("/createissue")
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces({MediaType.APPLICATION_JSON})
public Response createIssue(RestResourceModel requestData) {

    Long pageId = Long.parseLong(requestData.getPageId().getAsString());
    Page page = pageManager.getPage(pageId);

    // Read in current "issuekey" page property.  This works.
    ContentProperties contentProperties = page.getEntity().getProperties();
    String currentKey = contentProperties.getStringProperty("issuekey");
    System.out.println("Current issueKey: " + currentKey);

    // Set new value for "issuekey" and read it back.
    // It appears to work in the print statement but doesn't stick
    // outside the scope of this object.
    contentProperties.setStringProperty("issuekey", "TEST-123");
    String updatedKey = contentProperties.getStringProperty("issuekey");
    System.out.println("Updated pageid: " + page.getIdAsString() + " for issueKey: " + updatedKey);

    return Response.ok().build();
}

Here is where I read it in a macro:

public String execute(Map<String, String> parameters, String body, ConversionContext conversionContext) throws MacroExecutionException {

    // This does not return the updated property
	ContentProperties contentProperties = conversionContext.getEntity().getProperties();
	String issueKey = contentProperties.getStringProperty("issuekey");
	System.out.println("Page property from pageid: " + conversionContext.getEntity().getIdAsString() + " of issuekey is: " + issueKey);

	return "<div>" + issueKey + "</div>"
}

I suspect there is either a readonly flag on objects retrieved via the PageManager or there may be a scope discrepancy. Does anyone know how this should work?

Thanks
Michael


#11

Apparently setting a content property updates directly so the sentence below is answer is wrong
It doesn’t look like you update the page properties, you update the ContentProperties
also in the documentation of ContentProperties it states

Encapsulates the properties of a content object. You should never deal with the ContentProperty list directly. In fact, you probably want to use the ContentPropertyService and a JsonContentProperty instead.

Do it like this:

SaveContext DISCARD_MODIFICATION_DATA = new DefaultSaveContext(false, false, false);
String currentKey = page.getProperties().getStringProperty("issuekey");
System.out.println("Current issueKey: " + currentKey);
//Update it here
 page.getProperties().setStringProperty("issuekey", "TEST-123");
pageManager.saveContentEntity(content, DISCARD_MODIFICATION_DATA);
return Response.ok().build();

#12

What is the “content” of saveContentEntity()? Is it the page object retrieved from the PageManager?

I tried that but do end up throwing an exception:

INFO] [talledLocalContainer] java.lang.NullPointerException
[INFO] [talledLocalContainer] 	at com.atlassian.confluence.notifications.content.transformer.PageEditedPayloadTransformer.getOriginalId(PageEditedPayloadTransformer.java:24)
[INFO] [talledLocalContainer] 	at com.atlassian.confluence.notifications.content.transformer.PageEditedPayloadTransformer.getOriginalId(PageEditedPayloadTransformer.java:13)
[INFO] [talledLocalContainer] 	at com.atlassian.confluence.notifications.content.ContentEditedPayloadTransformer.checkedCreate(ContentEditedPayloadTransformer.java:18)
[INFO] [talledLocalContainer] 	at com.atlassian.confluence.notifications.PayloadTransformerTemplate.create(PayloadTransformerTemplate.java:45)

Thanks,


#13

Weird,
I got curious so I came with this sample code:

    ContentEntityObject cceo=conversionContext.getEntity();
	log.debug(cceo.getProperties().getStringProperty("FOOBAR"));
	String newFOOproerty="BAR_"+new Date();		
    cceo.getProperties().setStringProperty("FOOBAR","BAR_"+new Date());
	log.debug("SETTING FOO PROPERTY:"+newFOOproerty);

Here is the output of log:

Refresh 1:
2017-09-23 00:02:36,265 DEBUG [http-nio-8090-exec-8] [dcimteam.macros.SeeAlsoMacro] execute PageID:118001310 BAR_Sat Sep 23 00:00:40 CEST 2017
2017-09-23 00:02:36,269 DEBUG [http-nio-8090-exec-8] [dcimteam.macros.SeeAlsoMacro] execute PageID:118001310 SETTING FOO PROPERTY:BAR_Sat Sep 23 00:02:36 CEST 2017
Refresh 2:
2017-09-23 00:03:20,448 DEBUG [http-nio-8090-exec-1] [dcimteam.macros.SeeAlsoMacro] execute PageID:118001310 BAR_Sat Sep 23 00:02:36 CEST 2017
2017-09-23 00:03:20,450 DEBUG [http-nio-8090-exec-1] [.dcimteam.macros.SeeAlsoMacro] execute PageID:118001310 SETTING FOO PROPERTY:BAR_Sat Sep 23 00:03:20 CEST 2017
Refresh 3:
2017-09-23 00:04:07,474 DEBUG [http-nio-8090-exec-9] [dcimteam.macros.SeeAlsoMacro] execute PageID:118001310 BAR_Sat Sep 23 00:03:20 CEST 2017
2017-09-23 00:04:07,475 DEBUG [http-nio-8090-exec-9] [dcimteam.macros.SeeAlsoMacro] execute PageID:118001310 SETTING FOO PROPERTY:BAR_Sat Sep 23 00:04:07 CEST 2017

Seems to work for me, are you maybe getting exceptions?

Update: Running within macro the pagemanager.saveContentEntity seems to trigger some kind of loop, so avoid it.

Update2: I copied pasted your code. It works for me as expected:

Refresh 1:
execute Page property from pageid: 55476228 of issuekey is: null
Some rest endpoint
getPageTtree Current issueKey: null
 Updated pageid: 55476228 for issueKey: TEST-123
Refresh 2:
execute Page property from pageid: 55476228 of issuekey is: TEST-123

#14

Thanks so much for trying my code!

I’m seeing it work now but I’m not sure what I did other than an atlas-mvn clean and rebuild my h2 db. The save is working but still throwing an exception. I’ve cleaned up the code a bit for demo and isolated it in it’s own method.

The below is all in the REST endpoint. The pageid is passed to the endpoint as part of the data payload earlier in the code and I’ve confirmed it’s the same pageid as what the macro is running on.

    public Boolean updateStatus() {
        try {
            System.out.println("Response to JIRA POST: " + jsonResponse.toString());
            Long pageId = Long.parseLong(pageProperties.get("pageid").getAsString());
            Page page = pageManager.getPage(pageId);
            String issueKey = jsonResponse.get("key").getAsString();

            System.out.println("Response issuekey: " + issueKey + " PageId: " + pageId + " Page: " + page);
            SaveContext DISCARD_MODIFICATION_DATA = new DefaultSaveContext(false, false, false);
            String currentKey = page.getProperties().getStringProperty("issuekey");
            System.out.println("Current issuekey property on page: " + currentKey);
            page.getProperties().setStringProperty("issuekey", issueKey);
            String updatedKey = page.getProperties().getStringProperty("issuekey");
            System.out.println("Updated issuekey property on page before saving: " + updatedKey);

            pageManager.saveContentEntity(page, DISCARD_MODIFICATION_DATA);  // this is working but throwning an exception downstream!
            System.out.println("After saving pageid: " + page.getIdAsString() + " containts issuekey page property: " + page.getProperties().getStringProperty("issuekey"));

        } catch (Exception e) {
            System.out.println("Exception on updting status from JIRA response: " + e);
            return false;
        }

        return true;
    }

This gives me output:

INFO] [talledLocalContainer] Response to JIRA POST: {"id":"10405","key":"TEST-89","self":"http://localhost:2990/jira/rest/api/2/issue/10405"}
[INFO] [talledLocalContainer] Response issuekey: TEST-89 PageId: 1179652 Page: page: Sample Summary v.2 (1179652)
[INFO] [talledLocalContainer] Current issuekey property on page: TEST-88
[INFO] [talledLocalContainer] Updated issuekey property on page before saving: TEST-89
[INFO] [talledLocalContainer] 2017-09-23 17:22:55,231 ERROR [http-nio-1990-exec-7] [confluence.notifications.impl.NotificationsEventDispatcher] errorOrDebug Error during notification dispatch : null
[INFO] [talledLocalContainer]  -- referer: http://localhost:1990/confluence/display/ds/Sample+Summary | url: /confluence/rest/jirarequest/1.0/createissue | traceId: 2c245e1623fda4e4 | userName: admin
[INFO] [talledLocalContainer] java.lang.NullPointerException
[INFO] [talledLocalContainer] 	at com.atlassian.confluence.notifications.content.transformer.PageEditedPayloadTransformer.getOriginalId(PageEditedPayloadTransformer.java:24)
< a bunch more stack trace>
[INFO] [talledLocalContainer] After saving pageid: 1179652 containts issuekey page property: TEST-89

I’m catching all exceptions so I’m not sure why I don’t catch the NullPointerException above. It must be happening downstream. I’m concerned I’m not using the saveContentEntity() correctly.


#15

Remove the savecontententity method. The properties are updated without it. I was mistaken in the posts above


#16

In my system (Confluence 6.3.2), if I comment out the savecontententity, the properties don’t save. I believe you were right originally. However, since the exception is happening on the notification dispatch, I was able to avoid this by suppressNotifications=true.

SaveContext DISCARD_MODIFICATION_DATA = new DefaultSaveContext(true, false, false);

I’m not sure what downstream issues I will have with this but I’ll keep digging into it.


#17

For any future readers, I have got an issue saving my properties in a rest module (POST action) and was having the following exception:

[atlassian.spring.filter.FlushingSpringSessionInViewFilter] closeSession Unable to flush Hibernate session. Possible data loss: ClobStringType requires active transaction synchronization

By googling I eventually found out this thread and Panos reply was what I missed:

SaveContext DISCARD_MODIFICATION_DATA = new DefaultSaveContext(true, false, false);
contentEntityManager.saveContentEntity(spaceDescObj, DISCARD_MODIFICATION_DATA);

Glad that I found this !