Berichten met label trucs

Developing a Confluence Plugin

Introduction

Does your company use Confluence? If you’re looking to extend its possibilities, it may not seem obvious straight away which of the 30+ plugin module types to choose.

Say you want to add a form to Confluence in order to submit (and process) some data. Once the processing is done, the results should be displayed, if possible on the same page. If you’re interested in how to accomplish this, you probably want to keep on reading; it seems easy to create such a plugin, but there’s a few issues that you’re likely to run into.

In this blog I’ll describe the more troublesome/unexpected ones that we encountered and will explain how they were handled (or sometimes: worked around).

Plugin Module Types

First of all, let’s pick the plugin module types to use. In order to embed (HTML) content in a page, the plugin type to use is the Macro Plugin. This is not sufficient, though; for something to happen, the macro needs to display some form elements (e.g. text fields, select fields and a submit button) and when the form is submitted, the right action should take place. For this to happen, we used an XWork Action; the form data is sent to and handled by the Action.

First of all, you need to get a simple Macro Plugin up-and running. Besides creating an Archetype and some plugin-specific steps, most of the steps required to do this can be found here.

Following that, you need to setup an XWork Action (check the ‘XWork Action’ link above to find out more). Once that is done, create a velocity macro (.vm file) containing a form that submits its data to the Action:

    <form name="myForm" action="/confluence/plugins/actions/myaction.action" method="GET">
    	$action.inputText: <input type="text" name="myInput">
        <p>
        <input type="submit" value="Submit">
    </form>

Note that he action to be used (on a ’submit’ of the form) should be defined as the XWork Action ‘myaction’ in atlassian-plugin.xml.

After that, display the form by embedding (all text and controls defined in) the .vm file into the macro by updating its execute(…) method (used when the macro is rendered):

public String execute(Map params, String body, RenderContext renderContext) throws MacroException {
    Map context = MacroUtils.defaultVelocityContext();
    context.put("action", new MyAction());
    return VelocityUtils.getRenderedTemplate("templates/myaction.vm", context);
}

Note that the action that will handle the submitted form data is supplied through a “velocity context” map. Supplying it is only required if the action is queried when rendering the velocity macro (i.e. if you’re using elements in the .vm file that start with $action. – specifically, in this case: $action.inputText).

Also, it’s important to use only HTML body elements in the .vm file; a complete HTML syntax will result in anything on a page following the {macro} tag no longer being displayed! After this is done, it is possible to display / fill in the form (and submitting it to the XWork Action) through rendering the Macro that is wrapped around it.

Return from whence we came

As previously mentioned, we would like to ’stay’ in the same page. In order to achieve this, Confluence offers the possibility of a (server) redirect. Now, all we need to do is figure out from whence we came, since the macro can be embedded pretty much anywhere! Unfortunately, simply checking the request URL does not work (because of Confluence (pre)processing); a generic URL is returned (ending with /pages/viewpage.action). We solved it using a bit of a ’scrippety-trickery’ solution! Since we can’t find out the URL on the server-side, in the form that was submitted, we included a hidden element:

<input type="hidden" name="macroUrl">

and set it with the current URL by including a bit of JavaScript in the .vm file as well:

<script language="JavaScript">
	document.myForm.macroUrl.value = window.location
</script>

Don’t forget to add the matching getter and setter in the action that the velocity macro (form) posts to (in this case ‘MyAction’)! Once this is done, you can retrieve the Action value in the xwork/myaction part of the atlassian-plugin.xml configuration file (note that this is runtime info and not maven setting some properties!):

<action name="myaction" class="com.luminis.confluence.plugin.MyAction">
    <result name="success" type="redirect">${macroUrl}</result>
</action>

Persistent data

In order to store userspecific data, Confluence uses a table in which key-value combinations can be stored: the BANDANA table. Writing to and reading from this table is done through the BandanaManager.

To get hold of the correct manager objects, all you need to do is declare a variable and a setter for the variable and Spring will auto-inject the right managers:

    BandanaManager m_bandanaManager;
    public void setBandanaManager(BandanaManager bandanaManager) {
        m_bandanaManager = bandanaManager;
    }

If, at some point, you can’t wait for Spring to inject them, you can also look them up manually, e.g. for the PlatformTransactionManager:

    PlatformTransactionManager m_transactionManager;
    // ...
    Object o = ContainerManager.getComponent("transactionManager");
    if (o instanceof PlatformTransactionManager) {
        m_transactionManager = (PlatformTransactionManager)o;
    }

Finally, the BandanaManager uses a ‘context’ to get and set values:

    BandanaContext m_bandanaContext;
    // ...
    String rootKey = getClass().getPackage().getName();
    m_bandanaContext = new ConfluenceBandanaContext(rootKey);

As soon as the BandanaManager, the PlatformTransactionManager and the ConfluenceBandanaContext are available, it’s possible to store data into the BANDANA table:

    public void storeKeyValue(String key, Object val) {
        storeDataWithinTransaction(key, val);
        // Stopping and starting Confluence here is no problem.
        Object o = m_bandanaManager.getValue(m_bandanaContext, key);
        // o.equals(val) == true // assuming properly implemented 'equals()'
    }

    // Data should be stored within a transaction; use a Spring TransactionTemplate to wrap a transaction around the operation:
    public void storeDataWithinTransaction(final String key, final Object val) {
        TransactionTemplate tt = new TransactionTemplate(m_transactionManager);
        TransactionCallback callback = new TransactionCallback() {
            public Object doInTransaction(TransactionStatus transactionStatus) {
                m_bandanaManager.setValue(m_bandanaContext, key, val);
                return null;
            }
        };
        tt.execute(callback);
    }

Persistent ‘proprietary’ data

In case you want to define your own objects and store them as well, this will fail when using the BandanaManager, since it can’t find the class. Note that this is a tricky failure because after restarting Confluence, reading the value fails without any logging at all! Storing (new) values after that, however, does work. To make sure this does succeed, use XStream to convert the object to a(n XML) String before storing it:

    XStream m_xStream = new XStream();
    {
        m_xStream.setClassLoader(getClass().getClassLoader());
        m_xStream.alias("my-class", MyClass.class);
    }

    public void storeMyClass(MyClass mc) {
        String xmlStr = m_xStream.toXML(mc);
        storeKeyValue("specifickey", xmlStr);
    }

And convert it back from XML after reading it:

    public MyClass readMyClass() {
        String s = (String)m_bandanaManager.getValue(m_bandanaContext, "specifickey");
        return (MyClass)m_xStream.fromXML(s);
    }

, , , , , , , , ,

Nog geen reacties