Introduction

This tutorial aims at integrating REST functionality into a Confluence plugin. Its purpose is to provide a straightforward step-by-step documentation that is suited even for users who are just making their first experiences with the Confluence framework. Since the tutorial got quite comprehensive and contains many Confluence procedures of more general nature (e.g. creating macros, including web resources), it can also be considered a rather broad introduction to Confluence itself.

Outline:

  1. Prerequisites
  2. Setting up a Confluence project and Eclipse
  3. Adding REST dependencies to our pom.xml
  4. Creating Java entities which we can map from and to JSON with REST
  5. Creating a class for maintaining our beekeeping
  6. Creating the REST interface
  7. Defining the REST module in the atlassian-plugin.xml
  8. Creating a macro to provide user access to our beekeeping
  9. Including the macro in the atlassian-plugin.xml
  10. Creating a template for the user interface
  11. Implementing the communication to and from our REST methods with jQuery and Ajax
  12. Hook it all up and admire the result

1.) Prerequisites

This tutorial requires a Confluence SDK installed. If you don’t have the SDK yet, check out the first step of the official Atlassian “Getting Started” guide: https://developer.atlassian.com/display/DOCS/Installing+the+Atlassian+Plugin+SDK. We are using Eclipse as development IDE and certain steps in this tutorial are specifically associated with Eclipse. If you want to work through this tutorial, you should have basic knowledge on standard Web technologies like HTTP, HTML, JavaScript and the Java language.
Generally, we’re going to show only the relevant snippets of our code. You can download a .zip file containing the complete project at the bottom of the page.

2.) Setting up a Confluence project and Eclipse

  • Fire up a terminal and navigate into the folder where you want to create your Confluence project.
  • Run the command atlas-create-confluence-plugin
  • Specify a groupId - this should correspond to the package structure you want to use in your project. We are using de.scandio.confluence.plugins.
  • Specify an artifactId - this is going to be the name of your plugin. We are using resttutorial.
  • You can leave version and package at the default for now (just hit enter without input).
  • Confirm with y and hit enter.

Our project was now created in our desired directory. However, since we want to use Eclipse, we still must get the project Eclipse-ready. This is achieved as follows:

  • In the terminal, navigate into the directory that was just created for your project. The name corresponds to what you entered as artifactId, in our case resttutorial.
  • Run the command atlas-mvn eclipse:eclipse.
  • Open Eclipse and select File->Import…->Existing Projects into Workspace->Next. Navigate into the folder above your project folder and select the project. Click Finished.

Done. We created a Confluence plugin skeleton and imported it into Eclipse. Very well. Now we can start with the actual work.

3.) Adding REST dependencies to our pom.xml

Confluence needs some external libraries for REST functionality, which have to be added to the pom.xml file in the project’s root directory. Add the following dependencies between the <dependencies>...</dependencies> tags:

<dependency>
	<groupId>com.atlassian.plugins.rest</groupId>
	<artifactId>atlassian-rest-common</artifactId>
	<version>1.0.2</version>
	<scope>provided</scope>
</dependency>
<dependency>
	<groupId>javax.ws.rs</groupId>
	<artifactId>jsr311-api</artifactId>
	<version>1.1.1</version>
	<scope>provided</scope>
</dependency>
<dependency>
	<groupId>activemq</groupId>
	<artifactId>jaxb-api</artifactId>
	<version>20050407</version>
	<scope>provided</scope>
</dependency>
<dependency>
	<groupId>javax.servlet</groupId>
	<artifactId>servlet-api</artifactId>
	<version>3.0-alpha-1</version>
	<scope>provided</scope>
</dependency>

To make these dependencies and their contents work, we must now do the following:

  • In the project directory in the terminal, run the command atlas-clean
  • Run again atlas-mvn eclipse:eclipse
  • Right-click your project in Eclipse and hit Refresh

Good job. Now we have all the dependencies and components we need in our basket. Let’s do some real Confluence stuff right now!

4.) Creating Java entities which we can map from and to JSON with REST

For this tutorial we are going to build a little beekeeping to produce some good honey on the server. Users can then add as many bees as they want in order to make the beekeeping as efficient and delicious as possible.
To achieve this, we first need Java classes for out entity objects. These are transformed to and from JSON with RESTful client-server communication. For our project we first need a bee entity which we implement as a simple Java class:

// This is the package we defined as groupId.
package de.scandio.confluence.plugins;
// For using REST, we need annotations all over the place.
// They are used basically to provide meta information for REST calls and to hook the whole thing up.
// If you want to read stuff on annotations in general, refer to the Wikipedia oracle:
// http://de.wikipedia.org/wiki/Annotation_(Java).
import javax.xml.bind.annotation.*;
// This is our new class for Bee instances which will be mapped to and from JSON using REST.
// We need the XmlRootElement annotation such that REST later knows that these instances are
// supposed to be mapped from and to XML or JSON. Note: there is *NO* JsonRootElement annotation --
// XmlRootElement is used for JSON as well.
//
// Note: if you want to refer to these objects by a different name than the class name, you can
// add the optional name/namespace elements like so: @XmlRootElement(name = myownbee).
//
// We also use a XmlAccessorType annotation to indicate that all fields are automatically
// bound to JSON/XML.
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Bee {
	// The following are the attributes of each bean instance:
	// - name: the name of the bee
	// - description: a description of the bee
	// - honeypower: coefficient from 1 to 10, indicating how efficient the bee can produce honey
	// - speed: coefficient from 1 to 10, indicating how fast the bee can fly
	//
	// All attributes need the XmlElement annotation such that REST knows that these values must
	// be mapped to JSON/XML values. The name/namespace issue applies as mentioned previously,
	// for example, you could define @XmlElement(name = myownname).
	@XmlElement
	private String name;
	@XmlElement
	private String description;
	@XmlElement
	private int honeypower;
	@XmlElement
	private int speed;
	// We define a constructor for cases in which we want to create a bee manually.
	public Bee(String name, String description, int honeypower, int speed) {
		this.name = name;
		this.description = description;
		this.honeypower = honeypower;
		this.speed = speed;
	}
	// If we define a custom constructor, we are required categorically to define a
	// default constructor for REST.
	public Bee() { }
	// IMPORTANT: we need getters and setters for all attributes!
	// ... getters and setters ...
}

Since we want to fetch scores for our beekeeping from the server, we also need an entity that represents a beescore:

package de.scandio.confluence.plugins;
import javax.xml.bind.annotation.*;
// This class represents current scores for the beekeeping, namely total honeypower and
// speed of all bees.
// The class is defined with the same pattern as the Bee class.
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class BeeScore {
@XmlElement
private int totalhoneypower;
@XmlElement
private int totalspeed;
public BeeScore(int totalhoneypower, int totalspeed) {
this.totalhoneypower = totalhoneypower;
this.totalspeed = totalspeed;
}
public BeeScore() { }
// ... getters and setters ...
}

5.) Creating a class for maintaining our beekeeping

We need to store all bees that we added to our beekeeping and provide a possibility to retrieve the total scores in matters of honeypower and speed of all our bees. To make the content persistent across browser sessions, we use the internal Confluence BandanaManager which maintains a persistent key-value database.

package de.scandio.confluence.plugins;
import java.util.ArrayList;
import com.atlassian.bandana.BandanaContext;
import com.atlassian.bandana.BandanaManager;
import com.atlassian.confluence.setup.bandana.ConfluenceBandanaContext;
import com.atlassian.spring.container.ContainerManager;
// This class maintains our beekeeping. It provides methods for adding and removing bees and to
// calculate total honeypower and speed of all our bees. In here we use the BandanaManager which
// accesses the internal Confluence database.
public class Beekeeping {
	// We are using the Confluence BandanaManager for persistent storage. For information, see:
	// http://docs.atlassian.com/atlassian-bandana/0.2.0/com/atlassian/bandana/BandanaManager.html
	private BandanaManager bandanaManager;
	// The context for the BandanaManager.
	private final BandanaContext bandanaContext;
	// We implement this class as a singleton, such that we always refer to the same beekeeping
	// instance.
	private static Beekeeping INSTANCE;
	// Retrieve our beekeeping singleton instance. If it wasn't created yet, create it now.
	public static Beekeeping getInstance() {
		if (INSTANCE == null) {
			INSTANCE = new Beekeeping();
		}
		return INSTANCE;
	}
	// Our constructor is private, such that it can only be called from within our getInstance
	// method.
	private Beekeeping() {
		ContainerManager.autowireComponent(this);
		this.bandanaContext = new ConfluenceBandanaContext("beekeeping");
	}
	// Add a new bee to the Bandana database.
	public void addBee(Bee bee) {
		bandanaManager.setValue(this.bandanaContext, bee.getName(), bee);
	}
	// Remove a bee from the Bandana database.
	public void removeBee(String name) {
		bandanaManager.removeValue(this.bandanaContext, name);
	}
	// Get the total score honeypower and speed of all bees currently stored in the
	// Bandana database.
	public BeeScore getScore() {
		int honeypower = 0;
		int speed = 0;
		for (String name : this.bandanaManager.getKeys(this.bandanaContext)) {
			Bee bee = (Bee) this.bandanaManager.getValue(this.bandanaContext, name);
			honeypower += bee.getHoneypower();
			speed += bee.getSpeed();
		}
		return new BeeScore(honeypower, speed);
	}
	// Get all bees that are currently store in the Bandana database and return them as
	// ArrayList.
	public ArrayList getAllBees() {
		ArrayList bees = new ArrayList();
		for (String name : this.bandanaManager.getKeys(this.bandanaContext)) {
			bees.add((Bee) this.bandanaManager.getValue(this.bandanaContext, name));
		}
		return bees;
	}
	// Getters and setters for the BandanaManager are called by Confluence (injection).
	public BandanaManager getBandanaManager() {
		return bandanaManager;
	}
	public void setBandanaManager(BandanaManager bandanaManager) {
		this.bandanaManager = bandanaManager;
	}
}

6.) Creating the REST interface

In order to enable users to access the beekeeping and add their new bees, we need a REST service which connects functions invoked client-side to server-side methods for our beekeeping. We implement this as another Java class:

// This is the package we defined as groupId.
package de.scandio.confluence.plugins;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
// This is the REST service that defines the method that can be invoked by client-side
// functionality (esp. Ajax calls).
//
// First, we need to define a path for our service. This path is later reflected in the
// URL when we invoke the methods defined in this class. Since we only provide this REST
// interface, we leave this path at the root and use path names on the method level.
@Path("/")
public class BeeResource {
	// With this method we can add a new bee to our beekeeping. For each method in our
	// REST interface, we again need some annotations.
	// We are using the POST HTTP method which should be used if your method changes the
	// state of the server. This is what we do in this case, since we add a new bee to
	// the beekeeping.
	@POST
	// This is a Confluence-specific annotation on security. It says that the method can
	// be called without supplying user credentials. This is ok for demonstration purposes.
	@AnonymousAllowed
	// We want to give our method a bee as parameter which can be added to our beekeeping.
	// We provide the information for the bee as JSON. If configured properly, REST will
	// automatically convert the JSON string into a Bee object.
	@Consumes({MediaType.APPLICATION_JSON})
	// Now we need a path that invokes this method.
	@Path("/add-bee")
	public Response addBee(Bee bee) {
		// We retrieve our Beekeeping singleton instance...
		Beekeeping beekeeping = Beekeeping.getInstance();
		// ...and add our bee to it. In here, we don't care how our beekeeping finally stores
		// the new bee.
		beekeeping.addBee(bee);
		// If all went well, we can respond to the user with a simple OK status message.
		return Response.ok().build();
	}
	// This method removes a bee from our beekeeping. The annotations are the same as
	// previously.
	@POST
	@AnonymousAllowed
	@Consumes({MediaType.APPLICATION_JSON})
	@Path("/remove-bee")
	public Response removeBee(String name) {
		// This is exactly the same pattern as before.
		Beekeeping beekeeping = Beekeeping.getInstance();
		beekeeping.removeBee(name);
		return Response.ok().build();
	}
	// Using this method, we retrieve the total score for honeypower and speed of all bees
	// that are currently in the beekeeping. The annotations we need are a little different
	// this time.
	// We simply fetch something from the server and don't change its state. That's why we
	// use the HTTP GET method.
	@GET
	// Same as before.
	@AnonymousAllowed
	// This time we are not given any parameters but want to return a JSON-formatted BeeScore
	// object instead--we produce JSON.
	@Produces({MediaType.APPLICATION_JSON})
	@Path("/get-bee-score")
	public Response getBeeScore() {
		// We retrieve our Beekeeping singleton instance...
		Beekeeping beekeeping = Beekeeping.getInstance();
		// ...and calculate the current total BeeScore.
		BeeScore beeScore = beekeeping.getScore();
		// We send back the JSON-formatted score.
		return Response.ok(beeScore).build();
	}
}

7.) Defining the REST module in the atlassian-plugin.xml

In order to make the REST service work, we need to define a REST module in the atlassian-plugin.xml file in the resource directory of our project. We add the following:

p>
	<rest key="beekeeping-test-resources" path="/test-rest"
		version="1.0">
		<description>Provides the REST resource for the beekeeping.</description>
	</rest>

The path attribute will be reflected in the URL by which we access the REST methods.
Important note: we won’t use the key attribute throughout the project. The connection of the defined REST module and our Java classes is only achieved by the annotations in the classes for which Confluence scans the whole project.
Another note: if you want different paths to access your REST methods, you can define different rest modules in your atlassian-plugin.xml file. This way, you can even refer to methods within the same Java class with different REST modules and URLs.

8.) Creating a macro to provide user access to our beekeeping

Up to now, we covered a whole lot of concepts to show how REST works in a Confluence project. However, an important piece of the puzzle is still left–our application must be accessible by the users of our beekeeping, which means we need a user interface that contacts our REST service. We implement a Confluence Macro for this purpose, which the user simply can add in every Confluence page. Unfortunately, we have a few steps before us to achieve this, so let’s dive into it.
We create another Java class in our default package that contains our macro functionality.

// Our default package that refers to the project's groupId.
package de.scandio.confluence.plugins;
// Some imports we need for this macro. Most of them are default Confluence classes.
import java.util.ArrayList;
import java.util.Map;
import com.atlassian.confluence.content.render.xhtml.ConversionContext;
import com.atlassian.confluence.macro.Macro;
import com.atlassian.confluence.macro.MacroExecutionException;
import com.atlassian.confluence.pages.PageManager;
import com.atlassian.confluence.renderer.radeox.macros.MacroUtils;
import com.atlassian.confluence.spaces.SpaceManager;
import com.atlassian.confluence.util.velocity.VelocityUtils;
// This is the Java class connected to our Beekeeping macro. It is responsible for
// rendering our contents in the user interface and provides the functionality the
// user needs to maintain his or her beekeeping. In Confluence, every macro must
// implement the Macro interface, which we do with delight.
public class BeeMacro implements Macro
{
	// Standard Confluence macro attributes that are injected automatically.
    private final PageManager pageManager;
    private final SpaceManager spaceManager;
    // The constructor which handles Confluence injection.
    public BeeMacro(PageManager pageManager, SpaceManager spaceManager) {
        this.pageManager = pageManager;
        this.spaceManager = spaceManager;
    }
    // The execute method is called each time the macro is accessed in a Confluence page.
    @Override
    public String execute(Map parameters, String body,
    		ConversionContext context) throws MacroExecutionException {
    	// Retrieve the instance of our beekeeping.
    	Beekeeping beekeeping = Beekeeping.getInstance();
    	// Save all bees in an ArrayList data structure.
    	ArrayList bees = beekeeping.getAllBees();
    	// Create a new context for rendering...
    	Map renderContext = MacroUtils.defaultVelocityContext();
    	// ...and add our bees collection to it.
    	renderContext.put("bees", bees);
    	// Render our beekeeping Velocity template which, in the final jar, resides in the templates
    	// directory in the root of the project.
		return VelocityUtils.getRenderedTemplate("/templates/beekeeping.vm", renderContext);
    }
    // Standard macro methods that must be implemented.
    @Override
    public BodyType getBodyType() // Our macro doesn't need a body.
    {
        return BodyType.NONE;
    }
    @Override
    public OutputType getOutputType() // Our output type is simply block.
    {
        return OutputType.BLOCK;
    }
}

9.) Including the macro in the atlassian-plugin.xml

Our Confluence project needs to register the macro we created. Therefore, we must add it as a module to our atlassian-plugin.xml:

    <xhtml-macro name="beekeeping" class="de.scandio.confluence.plugins.BeeMacro" key="beekeeping-macro">
        <parameters/>
    </xhtml-macro>

Important: the class attribute must point to the macro class (BeeMacro) we just implemented.
After the macro has been set up, you can access it within each page in Confluence by using the { macro } syntax, in our case { Beekeeping }. After saving, the browser will render the template we’re going to implement in the next step.

10.) Creating a template for the user interface

As in every web page, the browser must finally be fed with some HTML, CSS and JavaScript to work with. We now write the Velocity template that serves this purpose. Velocity is the default templating engine in Confluence. Just chill, it’s just all about writing some HTML with a few program statements to read our bees out of the collection we provided in our macro.

</pre>
<div id="wrapper">
<div id="div-table" class="box">
<h2>Your Bees</h2>
## Put each bee from the data structure provided by the macro in our DOM table#foreach($bee in $bees)#end
<table id="table-bees">
<tbody>
<tr>
<th>Name</th>
<th>Description</th>
<th>Honey Power</th>
<th>Speed</th>
<th></th>
</tr>
<tr>
<td class="td-name">$bee.getName()</td>
<td>$bee.getDescription()</td>
<td>$bee.getHoneypower()</td>
<td>$bee.getSpeed()</td>
<td><a class="delete" href="#">[delete]</a></td>
</tr>
</tbody>
</table>
</div>
<div id="div-new" class="box">
<h2>New Bee</h2>
<span class="label">Name:</span><input id="input-name" type="text" />
<span class="label">Description:</span><input id="input-description" type="text" />
<span class="label">Honeypower:</span>
<select> <option>1</option><option>2</option><option>3</option><option>4</option><option>5</option></select>
<select> <option>6</option><option>7</option><option>8</option><option>9</option><option>10</option></select>
<span class="label">Speed:</span>
<select> <option>1</option><option>2</option><option>3</option><option>4</option><option>5</option></select>
<select> <option>6</option><option>7</option><option>8</option><option>9</option><option>10</option></select>
<a id="a-new" href="#">[add]</a>
<a id="a-score" href="#">[total score]</a></div>
</div>
<pre>

We also included some very basic CSS styles to make the interface not appear too ugly. Just use the <style>...</style> in your template.

11.) Implementing the communication to and from our REST methods with jQuery and Ajax

We can now access our REST service using jQuery and Ajax. First we add a JavaScript file in the same directory where we just created the template. We’ll call this file script.js.
To make the browser know where these web resources (mainly JavaScript and CSS) are located, we need to add another module to our atlassian-plugin.xml file like so:

p>
	<web-resource key="beekeeping-resources">
		<resource type="download" name="script.js" location="/templates/script.js"/>
	    <dependency>confluence.web.resources:ajs</dependency>
	    <context>atl.general</context>
	</web-resource>

The resource element adds our new script file to the module, the dependency element adds AJS (the Confluence JavaScript wrapper object) and jQuery (which is referenced with AJS.$). The context element says that we want to access these resources from everywhere within our plugin except from administration screens.
Now, we can add the JavaScript code to our script.js file. We wrap all our code into the AJS.toInit() function which is the Confluence way of the document.ready event in jQuery:

AJS.toInit(function() {
	// DOM is ready
});

As an example, we’ll do the Ajax functionality for the add-bee method in our REST service, which is accessible by the following URL schema:
http://myhost.com:port/myapp/rest/api-name/api-version/resource-name
We proceed the Ajax request within the click event listener of our [add] link.

AJS.$('#a-new').click(function(e) {
	// Prevent the default behavior of the browser -> do not follow the href link.
	e.preventDefault();
	// Create a JavaScript bee object from the inputs in the user interface.
	var bee = {
		name: AJS.$('#input-name').val(),
		description: AJS.$('#input-description').val(),
		honeypower: AJS.$('#select-honeypower').val(),
		speed: AJS.$('#select-speed').val()
	}
	// Use jQuery s ajax method to perform the ajax request.
	AJS.$.ajax({
		// This is a HTTP POST request.
		type: 'post',
		// The URL of the REST service according to the schema above. We can read the relative URL of our
		// application (in our case Confluence) with the AJS.Data.get method.
		url: AJS.Data.get('context-path') + '/rest/beekeeping-rest/1.0/get-bee-score',
		data: JSON.stringify(bee),
		// Set JSON encoding...
        contentType: 'application/json; charset=utf-8',
        // ...and JSON datatype.
        dataType: "json",
        // The success event indicates that the new bee was successfully stored in our beekeeping on the server.
        // If we refresh the browser, we can immediately see the new bee in our table. Since we do not want to
        // refresh each time we add a new bee, we add a new bee to our table with jQuery if the Ajax call
        // was successful.
        success: function(e) {
        	// Create the delete link for our the new table row
        	var a = AJS.$('<a class="a-delete" href="#">[delete]</a>');
        	AJS.$(a).click(deleteClick); // add the click listener.
        	var tr = AJS.$('
') // create the new table row and append the content.
        					.append(AJS.$('
').html(bee.name))
.append(AJS.$('
').html(bee.description))
.append(AJS.$('
').html(bee.honeypower))
.append(AJS.$('
').html(bee.speed))
        					.append(AJS.$('
').append(a));
        	AJS.$('#table-bees').append(tr);
        },
        // We display an error message if the request failed.
		error: function(e) {
			alert("Error executing ajax request.");
		}
	});
});

12.) Hook it all up and admire the result

Now that we proceeded along these steps (without_any_mistakes, eh - no, we don’t make mistakes), we can start our server.

  • In the project directory in the terminal, run the command atlas-run. This takes some time.
  • When the process is finished, browse the URL shown in the terminal.
  • Select any page in your plugin (probably you want to do this in the demonstration space page).
  • Enter our created macro with { Beekeeping } (the macro browser will automatically propose our macro).
  • If everything’s fine, you’ll see the template we defined for our macro and you can click through it and retrace what’s happening below the surface.

Conclusion

Yes, we’re done. We got our bees on the server producing honey with different speed and honey power. We can add and delete bees to and from our beekeeping. And we can retrieve total scores of all our bees in our beekeeping from the server. We achieved all this by implementing REST functionality in our Confluence plugin.

And yes, it was actually quite an amount of work. However, if the basic concepts explained in this tutorial are understood, there won’t be any difficulties embedding the steps into your custom Confluence project. The pattern is always constant and quite intuitive after a few repeats.

You can download the whole project as .zip archive  here.