Last updated Dec 8, 2017

Generating JSON output in Confluence with Jsonator

This document gives a technical overview of how JSON content can be generated in Confluence with the Jsonator framework.

The Jsonator framework makes it easy for Struts actions in plugins and Confluence core code to generate JSON for AJAX-related functionality in the web UI. It is also possible to extend the Jsonator framework in Confluence core code to provide custom serialisation for new types of objects.

Writing an action that generates JSON

To generate JSON from a Struts action, you map the result of the action to the 'json' result type. For example:

Struts action mapping

1
2
<action name="history" class="com.atlassian.confluence.user.actions.HistoryAction">
    <result name="success" type="json"/>
</action>

In your Struts action class, make sure it implements Beanable and the JsonResult provided by Confluence will invoke the getBean() method and automatically convert that into a JSON response.

Here is an example action class that returns a list of pages in the user's history:

Example JSON action class

1
2
public class HistoryAction extends ConfluenceActionSupport implements Beanable
{
    private ContentEntityManager contentEntityManager;
    private List<ContentEntityObject> history = new ArrayList<ContentEntityObject>();
    private int maxResults = -1;

    public String execute() throws Exception
    {
        UserHistoryHelper userHistoryHelper = new UserHistoryHelper(getRemoteUser(), contentEntityManager, permissionManager);
        history = userHistoryHelper.getHistoryContent(maxResults, new ContentTypeEnum[] { ContentTypeEnum.PAGE });
        return SUCCESS;
    }

    public Object getBean()
    {
        Map<String, Object> bean = new HashMap<String, Object>();
        bean.put("history", history);
        return bean;
    }

    public void setMaxResults(int maxResults)
    {
        this.maxResults = maxResults;
    }

    public void setContentEntityManager(ContentEntityManager contentEntityManager)
    {
        this.contentEntityManager = contentEntityManager;
    }
}

The two relevant parts of this action are:

  • validation and data loading are done in the execute() method, like any other action
  • the getBean() returns an Object (normally a List or Map) for serialisation into JSON.

Here is some sample output from this action, reformatted for readability:

Sample JSON output

1
2
{
    "history": [{
        "id": "111774335",
        "creationDate": "10 Dec 2007",
        "title": "Confluence Services",
        "creatorName": "pfragemann",
        "spaceName": "Confluence Development",
        "friendlyDate": "Aug 20, 2009",
        "lastModifier": "ggaskell",
        "spaceKey": "CONFDEV",
        "type": "page",
        "date": "Aug 20, 2009 15:48",
        "lastModificationDate": "20 Aug 2009",
        "url": "/display/CONFDEV/Confluence+Services"
    },
    {
        "id": "111774337",
        "creationDate": "10 Dec 2007",
        "title": "High Level Architecture Overview",
        "creatorName": "pfragemann",
        "spaceName": "Confluence Development",
        "friendlyDate": "Dec 10, 2007",
        "lastModifier": "pfragemann",
        "spaceKey": "CONFDEV",
        "type": "page",
        "date": "Dec 10, 2007 23:46",
        "lastModificationDate": "10 Dec 2007",
        "url": "/display/CONFDEV/High+Level+Architecture+Overview"
    }]
}

Adding JSON generation to an existing action

The normal process of converting an existing action to return JSON is as follows:

  1. Modify the action so it stores its data in a field on the object, if it doesn't already.
  2. Make the action implement Beanable and return the action's data in the getBean() method.
  3. Add a new Struts mapping (i.e. URL) for the action which maps all the action results to the "json" result type.
    • In Confluence core code, the new Struts mapping should be added to the /json Struts package.
    • In plugin code, the new Struts mapping can be added in any Struts package provided by the plugin.

As an example, the SearchSiteAction which provides Confluence's search functionality is accessed via the web UI in Confluence on /searchsite.action. To convert this to a JSON action, the following was done:

  1. The action was modified to implement Beanable and return the preexisting PaginationSupport class:

    1
    2
    public Object getBean()
    {
        Map<String, Object> result = new HashMap<String, Object>();
        result.put("total", paginationSupport.getTotal());
        result.put("startIndex", paginationSupport.getStartIndex());
        result.put("results", results);
        return result;
    }
    
  2. A new mapping was added to the /json/ package to expose the JSON functionality on /json/searchsite.action

    1
    2
    <action name="search" class="com.atlassian.confluence.search.actions.SearchSiteAction">
        <result name="success" type="json"/>
        <result name="error" type="json"/>
        <result name="input" type="json"/>
    </action>
    

Support serialisation objects

The DefaultJsonator includes implementations for the following types of objects (as of Confluence 3.3):

Primitive types and objects:

  • null
  • String
  • Number
  • Boolean
  • Byte
  • Collection (converted to JSON array with each element serialised individually)
  • Map (converted to JSON object, using key.toString() and serialising values indivudally)

Confluence objects:

The fallback for an object which is not of any of the above types is a generic JavaBean serialiser which will convert the object to a JSON object using any exposed JavaBean properties. Be careful of using this if objects returned from getBean() have non-trivial getter methods.

Customising object JSON serialisation

To add custom JSON serialisation in Confluence, you need to write a new implementation of the Jsonator interface and add it to the DefaultJsonator configuration in the applicationContext.xml Spring context.

The Jsonator interface (as of Confluence 3.3) looks like this:

1
2
package com.atlassian.confluence.json.jsonator;

import com.atlassian.confluence.json.json.Json;

/**
 * Interface to implement if you want to provide a method to create
 * a JSON representation of an object
 */
public interface Jsonator<T>
{
    /**
     * Creates a {@link Json} representation of a given object
     * @param object the object to be serialized
     * @return Json JSON representation of the given object
     */
    Json convert(T object);
}

Implementations must implement the single convert() method to return the associated JSON. The classes in the com.atlassian.confluence.json.json package should be used to generate the JSON.

As an example, here is the implementation of AttachmentJsonator:

1
2
public class AttachmentJsonator implements Jsonator<Attachment>
{
    private final HttpContext context;
    private final ThumbnailManager thumbnailManager;

    public AttachmentJsonator(HttpContext context, ThumbnailManager thumbnailManager)
    {
        this.context = context;
        this.thumbnailManager = thumbnailManager;
    }

    public Json convert(Attachment attachment)
    {
        JsonObject json = new JsonObject();
        json.setProperty("id", String.valueOf(attachment.getId()));
        json.setProperty("name", attachment.getFileName());
        json.setProperty("contentId", attachment.getContent().getIdAsString());
        json.setProperty("version", String.valueOf(attachment.getAttachmentVersion()));
        json.setProperty("type", attachment.getContentType());
        json.setProperty("niceSize", attachment.getNiceFileSize());
        json.setProperty("size", attachment.getFileSize());
        json.setProperty("creatorName", attachment.getCreatorName());
        json.setProperty("creationDate", attachment.getCreationDate());
        json.setProperty("lastModifier", attachment.getLastModifierName());
        json.setProperty("lastModificationDate", attachment.getLastModificationDate());
        json.setProperty("url", context.getRequest().getContextPath() + attachment.getUrlPath());
        json.setProperty("downloadUrl", context.getRequest().getContextPath() + attachment.getDownloadPath());
        json.setProperty("comment", attachment.getComment());

        try
        {
            ThumbnailInfo thumbnailInfo = thumbnailManager.getThumbnailInfo(attachment);
            json.setProperty("thumbnailUrl", thumbnailInfo.getThumbnailUrlPath());
            json.setProperty("thumbnailHeight", thumbnailInfo.getThumbnailHeight());
            json.setProperty("thumbnailWidth", thumbnailInfo.getThumbnailWidth());
        }
        catch (CannotGenerateThumbnailException e)
        {
            // ignore, attachment isn't the right type for thumbnail generation
        }
        return json;
    }
}

Currently, new Jsonators must be registered in the applicationContext.xml file, and it is not possible to add new ones via plugins. Here is the DefaultJsonator Spring bean declaration showing how the AttachmentJsonator is registered:

1
2
<bean id="jsonatorTarget" class="com.atlassian.confluence.json.jsonator.DefaultJsonator" scope="prototype">
    <constructor-arg index="0">
        <map>

            <!-- ... -->

            <entry key="com.atlassian.confluence.pages.Attachment">
                <bean class="com.atlassian.confluence.json.jsonator.AttachmentJsonator">
                    <constructor-arg index="0" ref="httpContext"/>
                    <constructor-arg index="1" ref="thumbnailManager"/>
                </bean>
            </entry>

            <!-- ... -->

        </map>
    </constructor-arg>
</bean>

Rate this page: