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).
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.
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.
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:
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:
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!)
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