Accessing form data from within macro's execute method

I am writing a macro for Confluence that contains a text input and a submit button within a form. When the form is submitted, I need to be able to access the data from the form in my macro’s main execute function*. How can I achieve this?

My macro’s code looks like this so far (take note of the code in the multi-line comment):

package com.vimeo.community.validator.macro;

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.user.AuthenticatedUserThreadLocal;
import com.atlassian.confluence.user.ConfluenceUser;
import com.atlassian.plugin.spring.scanner.annotation.component.Scanned;
import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
import com.atlassian.webresource.api.assembler.PageBuilderService;
import org.springframework.beans.factory.annotation.Autowired;

import java.lang.reflect.*;
import com.atlassian.confluence.core.ContentPropertyManager;
import com.atlassian.confluence.core.ContentEntityObject;
import com.atlassian.confluence.renderer.PageContext;

import java.util.Map;

@Scanned
public class validator implements Macro {

    private PageBuilderService pageBuilderService;
    private ContentPropertyManager contentPropertyManager;

    @Autowired
    public validator(@ComponentImport PageBuilderService pageBuilderService, @ComponentImport ContentPropertyManager contentPropertyManager) {
        this.pageBuilderService = pageBuilderService;
        this.contentPropertyManager = contentPropertyManager;
    }

    public String execute(Map<String, String> map, String s, ConversionContext conversionContext) throws MacroExecutionException {
        if (conversionContext.getPageContext() == null)
        {
            throw new MacroExecutionException("This macro can only be used in a page");
        }

        pageBuilderService.assembler().resources().requireWebResource("com.vimeo.community.validator.validator:validator-resources");
        ContentEntityObject contentObject = (conversionContext.getPageContext()).getEntity();

    /* The following code should only execute when I receive and verify specific posted form data.
        =================================================================================
        contentPropertyManager.setStringProperty(contentObject, "name", "Example Name");
        contentPropertyManager.setStringProperty(contentObject, "date", "1/2/13");
        =================================================================================
    */

        String lastValidationUser = contentPropertyManager.getStringProperty(contentObject, "name");
        String lastValidationTime = contentPropertyManager.getStringProperty(contentObject, "date");
        ConfluenceUser confluenceUser = AuthenticatedUserThreadLocal.get();

        String output = "<div class=\"validator-macro\">";

        if (lastValidationTime != null) {
            output = output + "<p>This article was last validated by " + lastValidationUser + " at " + lastValidationTime + ".</p>";
        } else {
            output = output + "<p>This article has not been validated.</p>";
        }

        output = output + "<form>";
        output = output + "<input type=\"text\" value\"This field will be hidden\" name=\"hidden-input\" />";
        output = output + "<button class=\"validate-button\" method=\"POST\" type=\"submit\">Validate</button>";
        output = output + "</form>";
        output = output + "</div>";

        return output;
    }

    public BodyType getBodyType() { return BodyType.NONE; }

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

*The reason I need to access it from the execute function is because I need to use the submitted data to update some properties on my ContentEntityObject. This requires the use of ContentPropertyManager interface, which I can only access from within my macro (via @ComponentImport).

Dear @zena

If I understand your problem correctly, you can not handle the form submit in the macro itself. The execute() method is called, when Confluence renders the page and includes the result into the page, which is sent to the browser and the lifecycle of the macro ends.

So I can imagine two ways to archive what you want:

  • You write an additional servlet, which receives the form submit and the servlet processes the data (writing it into the ContentEntityObject of the page) and afterwards make a redirect to the page, in order the page is rendered again.

  • Instead of the Servlet you also can implement it with a REST service, which is called by javascript, when the form is submitted. Afterwards you have to reload the page, to see the results.

In both cases, you have to send the page id with the form submit, so the service or servlet can determine the correct page.

Regards,
Alexander

1 Like

You have misunderstood what a macro does.
I ll try to give you an idea…
We got 3 main ways to interact and present data:
Macro, Action, REST api.
A macro is something used only to present data within a confluence page through xhtml or within a programmatically generated page (VM and soy) through “macro” declaration.
Action is used mainly to render a whole custom page, and form submission.
REST is used for form submission and data representation by rendering dynamically html.

There are a couple of differences between action and rest, but since you are learning my tip is use REST for submission and data retrieval. Use action for page rendering.

@alexander.kueken, @Panos,

Maybe this is the missing piece to my puzzle…

If I use a REST service, and trigger it by making a request via JavaScript, how can I write the data into the ContentEntityObject of a particular page? A REST endpoint is not associated with any particular page, so how do I retrieve the right ContentEntityObject and write data into it?

In the macro’s execute function, I can get the ContentEntityObject by doing this:

ContentEntityObject contentObject = (conversionContext.getPageContext()).getEntity();

I then need to inject an instance of the ContentPropertyManager into the class and use that to set a property on contentObject.

contentPropertyManager.setStringProperty(contentObject, "name", "Example Name");
contentPropertyManager.setStringProperty(contentObject, "date", "1/2/13");

How can I do this within my REST method?

As long as you have the id of the page, you can, for example, use the PageManager to retrieve the page, instead of using the conversionContext.

@alexander.kueken,

I’m sorry to be so thick, but I tried doing this exact thing and couldn’t figure out how to get the page’s context from the actual Page object. How do I do that?

And once I have the context, how can I get access to instances of PageManager and ContentPropertyManager? Can I just instantiate them using new?

I get the following errors when I try to instantiate instances of the PageManager and ContentPropertyManager:

[ERROR] /Users/hirschz/Dropbox/projects/atlassian_apps/validator/src/main/java/com/vimeo/community/validator/rest/ValidatorRest.java:[40,35] com.atlassian.confluence.pages.PageManager is abstract; cannot be instantiated
[ERROR] /Users/hirschz/Dropbox/projects/atlassian_apps/validator/src/main/java/com/vimeo/community/validator/rest/ValidatorRest.java:[41,57] com.atlassian.confluence.core.ContentPropertyManager is abstract; cannot be instantiated

No problem, sadly I can not provide you some working code right now.

Using the PageManager you get a Page object instead of the generic ContentEntityObject (Page is a subtype of it). So you do not need the Context if you use the PageManager.

You can not instantiate the PageManager with new. Instead you have to inject it with @ComponentImport

So in your Servlet or your REST service, you have to inject the PageManager and the ContentPropertyManager. Your code would look something like that then:

ContentEntityObject page = (pageManager.getPage(pageID)));
contentPropertyManager.setStringProperty(page, "name", "Example Name");
contentPropertyManager.setStringProperty(page, "date", "1/2/13");
1 Like

Okay—I didn’t realize I could inject things with @ComponentImport into my REST service. I have to find the exact syntax for doing that, but if it works, I think that should solve my problems.

Thank you so much for your help! (and @Panos too!)

Zena

Yes you can use the same syntax as in every other plugin module:

@Path("/myresource")
@Scanned
public class MyResource {

    @ComponentImport
    private final PageManager pageManager;

    @ComponentImport
    private final ContentPropertyManager contentPropertyManager;

    public MyResource(PageManager pageManager, ContentPropertyManager contentPropertyManager) {
        this.pageManager = pageManager;
        this.contentPropertyManager = contentPropertyManager;
    }

  @PUT
    @Consumes(MediaType.APPLICATION_JSON)
    public final Response put(final String json, @Context HttpServletRequest request) {

       ...
ContentEntityObject page = (pageManager.getPage(pageID)));
contentPropertyManager.setStringProperty(page, "name", "Example Name");
contentPropertyManager.setStringProperty(page, "date", "1/2/13");
...
    }

Thank you, thank you, thank you!

I’ve implemented what I think is correct, but I’m getting a failed test with the following output:

-------------------------------------------------------------------------------
Test set: ut.com.vimeo.community.validator.rest.ValidatorRestTest
-------------------------------------------------------------------------------
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.015 sec <<< FAILURE!
messageIsValid(ut.com.vimeo.community.validator.rest.ValidatorRestTest)  Time elapsed: 0.013 sec  <<< ERROR!
java.lang.NoSuchMethodError: com.vimeo.community.validator.rest.ValidatorRest: method <init>()V not found
	at ut.com.vimeo.community.validator.rest.ValidatorRestTest.messageIsValid(ValidatorRestTest.java:28)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:30)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
	at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:252)
	at org.apache.maven.surefire.junit4.JUnit4Provider.executeTestSet(JUnit4Provider.java:141)
	at org.apache.maven.surefire.junit4.JUnit4Provider.invoke(JUnit4Provider.java:112)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.apache.maven.surefire.util.ReflectionUtils.invokeMethodWithArray(ReflectionUtils.java:189)
	at org.apache.maven.surefire.booter.ProviderFactory$ProviderProxy.invoke(ProviderFactory.java:165)
	at org.apache.maven.surefire.booter.ProviderFactory.invokeProvider(ProviderFactory.java:85)
	at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:115)
	at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:75)

Any ideas why? My code looks like this:

package com.vimeo.community.validator.rest;

import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
import com.atlassian.confluence.pages.PageManager;
import com.atlassian.confluence.mail.notification.ConversionContextCreator;
import com.atlassian.confluence.content.render.xhtml.ConversionContext;
import com.atlassian.confluence.core.ContentEntityObject;
import com.atlassian.confluence.core.ContentPropertyManager;

import com.atlassian.plugin.spring.scanner.annotation.component.Scanned;
import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
import org.springframework.beans.factory.annotation.Autowired;

import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("/validate")
@Scanned
public class ValidatorRest {

    @ComponentImport
    private final PageManager pageManager;

    @ComponentImport
    private final ContentPropertyManager contentPropertyManager;

    public ValidatorRest(PageManager pageManager, ContentPropertyManager contentPropertyManager) {
        this.pageManager = pageManager;
        this.contentPropertyManager = contentPropertyManager;
    }

    @PUT
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public final Response put(final String json) {   
        long pageId = 3538946;
        ContentEntityObject page = (pageManager.getPage(pageId));
        contentPropertyManager.setStringProperty(page, "name", "Set from API handler");
        contentPropertyManager.setStringProperty(page, "date", "1/2/13");
        
        return Response.ok(new ValidatorRestModel("Hello World")).build();
    }
}

Your code is fine. Your tests failing. Are you using some IDE?

Look at the error message.

java.lang.NoSuchMethodError: com.vimeo.community.validator.rest.ValidatorRest: method ()V not found
at ut.com.vimeo.community.validator.rest.ValidatorRestTest.messageIsValid(ValidatorRestTest.java:28)

Notice that the class that tests (ValidatorRestTest) and the package it belongs to (ut.com.vimeo.community.validator.rest)

If you navigate to test folder using the IDE you should see two packages:
ut.com.vimeo.community.validatorand it.com.vimeo.community.validator These are for unit and integration tests.

In your other question, I wrote you that atlas-create-confluence-plugin-module creates 2 testfiles in the REST option. There you go :slight_smile:

Ah, I see, I had to remove the test that was for the old, default functionality in the REST module. That solved the problem.

I’m still having trouble getting the endpoint to work (still getting a 404 when I access it via JavaScript), but I’ll open a new topic for that.

Thanks again for all your help.

Put it in a github repo and i’ll take a look at it

Here it is… thank you!
https://github.com/zenahirsch/confluence-validator

Remove the first “validator” in your js to be like the following and update path in atlassian-xml to path="/vimeo-validator"

$.post('/rest/vimeo-validator/1.0/validate', function (response) {
    console.log(response);
});

and works like a charm. Seems the namespace “validator” is already taken.

@Panos, sadly, it’s still not working. Still getting a 404. I’ve updated the code in the repo so you can see my changes.

Check again, is it 404? For me it works though gives 500 error, but that’s another problem

It’s definitely 404 for me. I’ve tried many times. :confused:

I started a new thread here, FYI: Receiving 404 when making request to REST endpoint