Building an Atlassian Confluence plugin without Atlas, et al

Note: This posting has been updated so as to work with Confluence 4.x. (The previous version of the posting targeted Confluence 2.x.)

I spent several semi-productive hours this weekend writing a plugin for Atlassian's Confluence wiki. The plugin enables the running of JavaScript on the server and placing the result into the page displayed. I have found that using a wiki to intermix static content and dynamic content makes for a great reporting and situation-awareness tool kit (see JSPWiki). I wanted to do the same again within the confines of Confluence 4.x.

Confluence has a really good plugin manager webapp. You can search a repository for existing plugins for immediate inclusion. And you can also upload the plugin from your desktop. I gave the plugin manager a work out getting my plugin to work and can attest to its stability.

And here is the rub, Atlassian has made including and using plugins simple but has made creating a simple plugin almost impossible. My plugin's actual logic is very simple


private static final ScriptEngineManager factory = new ScriptEngineManager();
...
ScriptEngine engine = factory.getEngineByName("JavaScript");
Object result = engine.eval(script);
return result.toString();

To make this happen, however, Atlassian wants me to use Atlas. Atlas, as a far as I can tell, is much like Ruby on Rails and Spring Roo where the tool lays out a directory structure with files that together are the parts of and tools for building an "Hello World" application/plugin. In addition to Atlas, Maven and (perhaps) Eclipse are also needed. If my occupation was building whole applications on top of the Atlassian products and their APIs I could understand the logic of this tool chain. But I have a 4 line (!) plugin.

Confluence has been around for a long time and I guessed that the pre-Atlas way of building plugins was documented and with code examples. As far as I can tell, and this is after much searching, this is not the case. I was not able to find a single example of a basic plugin built with, for example, Ant. Since, in the end, a plugin is nothing more than a jar containing code and configuration I was shocked that this was missing from the mass of other documentation Atlassian provides. A whole population of, mostly in-house, programmers are being ignored. These are the programmers that are going to build plugins, i.e. small extensions to big tools that aid the better match between Confluence and the users needs.

To this end, here is how I build a basic macro plugin. (Note that the plugin documented here is not what I finally created. In the process of using the JDK's implementation of JavaScript I discovered that it is a old version of Mozilla's Rhino that does not support E4x, the XML language extensions. E4X makes XML a first class data type within JavaScript. Even the JavaScript syntax has been extends to allow for XML constants, for example x = <a/>. And so the final plugin uses Rhino 1.7R3 which does support E4X and JavaScript 1.8.)
The plugin's jar needs a minimum of two files. The Java class and the atlassian-plugin.xml configuration file. The development directory tree is
.
./build.xml
./src/atlassian-plugin.xml
./src/com/andrewgilmartin/confluence/plugins/script/JavaScriptPlugin.java

The JavaScriptPlugin class extends BasicMacro and must override the methods isInline(), hasBody(), getBodyRenderMode() and execute(). The isInline method specifies if the output of the plugin is suitable for an HTML span or block. The hasBody method specifies if the plugin has content, for example, as does the {code} macro. The getBodyRenderMode() specifies how Confluence is to handle the macro's output. Returning RenderMode.COMPATIBILITY_MODE specifies that the output is wiki text to be rendered as HTML. And, finally, execute does the work of the plugin.


package com.andrewgilmartin.confluence.plugins.script;

import java.util.Map;
import com.atlassian.renderer.RenderContext;
import com.atlassian.renderer.v2.macro.BaseMacro;
import com.atlassian.renderer.v2.macro.MacroException;
import com.atlassian.renderer.v2.RenderMode;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class JavaScriptPlugin extends BaseMacro {

    private static ScriptEngineManager factory = new ScriptEngineManager();

    @Override
    public boolean isInline() {
        return true;
    }

    @Override
    public boolean hasBody() {
        return true;
    }

    @Override
    public RenderMode getBodyRenderMode() {
        return RenderMode.COMPATIBILITY_MODE;
    }

    @Override
    public String execute(Map params, String body, RenderContext renderContext) throws MacroException {
        try {
            ScriptEngine engine = factory.getEngineByName("JavaScript");
            engine.put("params", params);
            Object evalResult = engine.eval(body);
            String result = evalResult.toString();
            return result;
        }
        catch (ScriptException e) {
            throw new MacroException(e);
        }
    }
}

// END

The atlassian-plugin.xml is an XML declaration of the plugin. It must be in the root of the plugin's jar file. The file contains two main sections. The first is plugin-info which declares the plugin as a whole. The second is the (repeatable) macro which declares the specific plugin. The atlassian-plugin.xml has several further refinements of these sections. I was not able to find an XML schema for this file type.

What I have discovered about it is from reviewing Atlassian's own plugins. The atlassian-plugin element's key attribute seems to be a unique identifier but does not have a prescribed structure: I am here just using the JavaScritMacro class's package name. The macro element's name attribute is the name of the macro as used by the user. For example {javascript}1+2+3{javascript}

<atlassian-plugin
 name="JavaScript Macro Plugin"
 key="com.andrewgilmartin.confluence.plugins.script">

    <plugin-info>
        <description>A JavaScript macro plugin</description>
        <vendor name="Andrew Gilmartin" url="http://www.andrewgilmartin.com" />
        <version>1.0</version>
    </plugin-info>

    <macro
 name="javascript"
 class="com.andrewgilmartin.confluence.plugins.script.JavaScriptPlugin"
 key="com.andrewgilmartin.confluence.plugins.script.javascript">
        <description>A JavaScript macro plugin. Place the script to execute within the body of the macro.</description>
    </macro>

</atlassian-plugin>

The build script is


<project name="com.andrewgilmartin.confluence.plugins.script" default="dist">

    <property environment="env" />

    <path id="build.classpath">
        <fileset dir="${env.HOME}/lib/atlassian-confluence-4.3.3/confluence/WEB-INF/lib">
            <include name="**/*.jar"/>
        </fileset>
        <pathelement location="${basedir}"/>
    </path>

    <target name="dist">
        <javac
            classpathref="build.classpath"
            srcdir="${basedir}/src"
            destdir="${basedir}/src"
            source="1.5"
            target="1.5"/>
        <jar
            destfile="${basedir}/confluence-plugins-script.jar"
            basedir="${basedir}/src"/>
    </target>

    <target name="clean">
        <delete>
            <fileset dir="${basedir}" includes="**/*.class"/>
            <fileset dir="${basedir}" includes="**/*.jar"/>
        </delete>
    </target>

</project>

Replace ${env.HOME}/src/confluence-2.10.4-std/confluence/WEB-INF/lib/ with the location of your Confluence jar files.
For more information about building out your plugin do read the documentation and review the code for Atlassian's own plugins, for example Confluence Basic Macros, Confluence Advanced MacrosConfluence Information Macros and Chart Macro.

Atlassian uses the Spring toolkit in their development. Since Spring performs dependency-injection of objects matching the results of using Java reflection to find "bean" names, a lot of Atlassian's code looks like magic is happening. That is, there is no visible configuration or other assignment of object to values and yet the assignments have to made for the code to work. Spring is the magician.
And that is it. The built jar file is a valid Confluence 4.x plugin. Happy wiki scripting
Download an archive of the development tree (Thanks to David Wilkinson for the tool to create this data URI.)

Coda: I was asked why I created my own scripting plugin when two already exist in Atlassian's plugin repository. The initial reason was that our MySql 5.0 installation has too small a max_allowed_packet size and so Confluence was not able to install the existing script plugins into the database. The ultimate reason was that I knew what I wanted from the plugin and said to myself "how hard can it be?"










Tools & notes

What I am focusing on in this photo is that he is using a computer and a notebook. I do the same. It suddenly occurred to me that every crafts person, since the beginning of time, uses this work arrangement. Tool to one side and notes to the other side. This is a great pattern. Why do we not perpetuate it today? Why put both on one machine? Some places have. When I worked at Lotus we always had a "development" machine and an "administration" machine. Perhaps some places do.

I should be able to open my laptop for use on one side of the desk and then slip out the tablet to place on the other side. Just say'n.

Ubuntu

I am because we are.

Horizon charts

I have been slowly working on a metrics collection and monitoring service. There are many others, but I wanted something very simple to feed and to integrate with a wiki to monitor/display. During the development of the service I discovered Horizon charts by way of (the brilliant) Cubism.js. The goal of an horizon chart is to use the a minimum of vertical height without loss of precision. Horizon charts look like bar charts. However, horizon charts use both the top and bottom edge as axises. The top edge is used to show values below a threshold with the bars going downward and the bottom edge is used to show values above the threshold with the bars going upward. Further, to extend the range of a value beyond the height of the chart the values are "folded" and the folds layered on the chart. Each fold is drawn as a bar on top of the previous fold's bar. The illustration at Sizing the Horizon: The Effects of Chart Size and Layering on the Graphical Perception of Time Series Visualizations shows this very well: I was a little intimidated by Cubism.js and D3.js; they are sophisticated toolkits that will take more time for me to understand than I wanted to commit just now. Plus I really wanted to learn more about using HTML's canvas element, and so I set about my own implementation. The code at https://gist.github.com/4264347 is my first cut at the implementation. It works for a fixed, two fold horizon chart. The example chart below plot the values between +/-200 with folds at +/-100.
 

Update: Updated the working example to be more general. Removed the code from this posting in preference to the Gist. 

FYI: To run the code just download the gist and open it in a browser. To play with it copy the gist into the HTML panel of https://jsfiddle.net/ and run.