Last updated Dec 8, 2017

Writing a macro using JSON

Applicable:

This tutorial applies to Confluence 4.x and later

Level of experience:

This is an intermediate tutorial. You should have completed at least one beginner tutorial before working through this tutorial. See the list of tutorials in DAC.

Time estimate:

It should take you approximately 1 hour to complete this tutorial.

Overview of the tutorial

In this tutorial, you create a Confluence macro that prints out the days of the week starting on a given date. If a user supplies a date as a macro parameter, the macro prints the days of the week starting on that date. Otherwise, it shows the the days of the week starting with the current date.

While the calendar itself isn't very useful, it will demonstrate how to use the Confluence JSON APIs from the plugin Java code. Our calendar will be composed by JSON data.

About these Instructions

You can use any supported combination of OS and IDE to construct this plugin. These instructions were written using IDEA on Mac OS X. If you are using another combination, you should use the equivalent operations for your specific environment.

This tutorial was last tested with Confluence 5.1.4.

Required knowledge

To get the most out of this tutorial, you should be familiar with:

  • The basics of Java development: classes, interfaces, methods, how to use the compiler, and so on.
  • How to create an Atlassian plugin project using the Atlassian Plugin SDK.
  • How to use and administer Atlassian Confluence.

Also, while this tutorial demonstrates how to work with JSON data in a plugin, it does not discuss JSON itself. For more information on JSON, see http://www.json.org/.

Plugin source

We encourage you to work through this tutorial. If you want to skip ahead or check your work when you have finished, you can find the plugin source code on Atlassian Bitbucket. Bitbucket serves a public Git repository containing the tutorial's code. To clone the repository, issue the following command:

1
2
hg clone https://bitbucket.org/atlassian_tutorial/writing-a-macro-using-json

Alternatively, you can download the source as a ZIP archive by choosing download here: bitbucket.org/atlassian_tutorial/writing-a-macro-using-json

Step 1. Create the plugin project

In this step, you'll use the Atlassian Plugin SDK to generate the scaffolding for your plugin project. The Atlassian Plugin SDK automates much of the work of plugin development for you. 

  1. If you have not already set up the Atlassian Plugin SDK, do that now: Set up the Atlassian Plugin SDK and Build a Project.

  2. Open a terminal and navigate to your Eclipse workspace directory.

  3. Enter the following command to create the project files for the plugin:

    1
    2
    atlas-create-confluence-plugin
    
  4. As prompted, enter the following project parameters:

    group-id

    com.example.plugins.tutorial.confluence

    artifact-id

    examplemacro

    version

    1.0-SNAPSHOT

    package

    com.example.plugins.tutorial.confluence

  5. Confirm your entries when prompted.

Step 2. Review the POM

The SDK generated a project directory with most of the artifacts our plugin needs, including a POM (Project Object Model) file. The POM describes your project and declares your plugin build dependencies. It's a good idea to open and review the POM after generating a new project. Let's do that now, making a few changes to the generated content as we go. 

  1. Change to the examplemacro directory created by the SDK in the previous step and open the file pom.xml.

  2. Add your company or organisation name and your website to the organization element:

    1
    2
    <organization>
      <name>Example Company</name>
      <url>http://www.example.com/</url>
    </organization>
    
  3. Update the description element:

    1
    2
    <description>This plugin illustrates how to use JSON in a macro.</description>
    
  4. Save the file.

Step 3. Tweak the plugin descriptor

Next we'll make a few changes to the plugin descriptor file. Among other things, the descriptor tells Confluence what UI elements the macro wants to extend or implement. In the Atlassian plugin framework, a unit of functionality is encapsulated by a module. For our plugin, we'll modify the Web Resource module that the SDK gave us and add a type of Macro Modulexhtml-macro:

  1. Open the plugin descriptor file, atlassian-plugin.xml, for editing. It is located in the src/main/resources directory of your project home.
    Notice that the SDK added a few module for our plugin already. For one, it added a web-resource element with a few resources that plugins typically require. We won't use all of them, but we will use one or two and add another.

  2. Add the following resource element to the web-resource element in the descriptor:

    1
    2
    <resource type="velocity" name="template" location="/templates/examplemacro.vm"/>
    

    This is the Velocity template that we'll create in a bit.

  3. Add the following as a child of atlassian-plugin:

    1
    2
    <xhtml-macro name="examplemacro" class="com.example.plugins.tutorial.confluence.ExampleMacro" key="my-macro">
        <parameters>
            <parameter name="date" type="string"></parameter>
        </parameters>
        <description>Inserts a calendar for the week into the current page</description>
    </xhtml-macro>
    

    This is the module declaration for our macro. Notice that it's going to take a single parameter, date

  4. Save your changes.

Step 4. Add presentation resources

Our macro generates its presentation through a JavaScript and a Velocity resource. The SDK gave us a default JavaScript file we can use. The Velocity template we'll have to add. Do that now: 

  1. Navigate to the resources directory, src/main/resources, and create a new directory there named templates.

  2. In the templates directory you just created, create a file named examplemacro.vm and add the following content to the file:

    1
    2
    #requireResource("confluence.web.resources:jquery")
    #requireResource("com.example.plugins.tutorial.confluence.examplemacro:examplemacro-resources")
    
    <div class="exampleMacro">
        <fieldset class="parameters hidden">
                <input type="hidden" class="dayDates" value="$dayDateJson">
        </fieldset>
    </div>
    

    Notice that our Velocity template consists of resource statements and a div with a single fieldset element. The element contains a hidden input element, to which we assign the output of our macro's execute method (that is, the URL-encoded, serialized JSON object string). The requireResource statements include the JavaScript file we'll create for this macroand the jQuery JavaScript framework.

  3. Open the properties file in the resources directory, examplemacro.properties, and add the following properties to the file:

    1
    2
    com.example.plugins.tutorial.confluence.examplemacro.examplemacro.param.date.label=Start Date:
    com.example.plugins.tutorial.confluence.examplemacro.examplemacro.param.date.desc=Enter date in DD/MM/YYYY format (e.g., 03/22/2014)
    

    These will provide the label and description for the parameter in the macro browser, as shown here:

  4. Open src/main/resources/js/examplemacro.js and add the following content:

    1
    2
    jQuery(function ($) {
        var initExampleMacro = function ()
        {
            $(".exampleMacro").each(function()
            {
            var dayDates = $(this).find("input.dayDates").val();
            var json;
            // determine if the browser has native JSON parser support & create JSON object
            if (typeof (JSON) !== 'undefined' && typeof (JSON.parse) === 'function')
            {
                json = JSON.parse(decodeURIComponent(dayDates).replace(/\+/g, '\u00a0'));
            } else {
                json = eval('(' + decodeURIComponent(dayDates).replace(/\+/g, '\u00a0') + ')');
            }
            // create table
            var html = "<table border=\"1\"><tr><th>DAY</th><th>DATE</th></tr>";
            for (var i=0; i<7; i++)
            {
                html = html + "<tr><td>" + json.daysdates[i].day + "</td><td>" + json.daysdates[i].date + "</td></tr>";
            }
            html = html + "</table>";
            $(this).html(html);
        });
    };
    $(document).ready(function()
        {
            initExampleMacro();
        });
    });
    

In most cases, a plugin would generate HTML in a Velocity template rather than in JavaScript as we've done here. However, we've included HTML in the JavaScript code just to keep our plugin simple, with the emphasis being on how to access values from JSON objects.

Our JavaScript code:

  • Retrieves the URL-encoded, serialized JSON string from the hidden input element.
  • Creates a JSON object from that string using jQuery.parseJSON, replacing any spaces with non-breaking spaces in the process.
  • Creates HTML for a table with a row for each of the seven dates and two columns for the dates and days, populating each cell with the relevant values from the JSON object
  • Inserts the HTML into the page.

Of course, our JavaScript and Velocity presentation resources don't have much to work with yet, as we still haven't coded the macro logic. You'll start on that next.

Step 5. Create the macro class

Let's add the Java class that implements our macro logic. We'll start simple, creating a macro that just generates placeholder text for now.

  1. Create a file named ExampleMacro.java. in the src/main/java/com/example/plugins/tutorial/confluence directory.

  2. Add the following skeleton code: 

    1
    2
    package com.example.plugins.tutorial.confluence;
    
    import java.util.Map;
    import java.util.List;
    
    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.pages.Page;
    import com.atlassian.confluence.spaces.SpaceManager;
    import com.atlassian.confluence.user.AuthenticatedUserThreadLocal;
    import com.atlassian.user.User;
    import com.opensymphony.util.TextUtils;
     
    import java.text.DateFormat;
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Calendar;
    import java.util.Date;
    import com.atlassian.confluence.json.json.JsonArray;
    import com.atlassian.confluence.json.json.JsonObject;
    import com.atlassian.confluence.renderer.radeox.macros.MacroUtils;
    import com.atlassian.confluence.util.GeneralUtil;
    import com.atlassian.confluence.util.velocity.VelocityUtils;
    import org.apache.velocity.VelocityContext;
    
    public class ExampleMacro implements Macro
    {
      private final PageManager pageManager;
      private final SpaceManager spaceManager;
    
      public ExampleMacro(PageManager pageManager, SpaceManager spaceManager)
      {
        this.pageManager = pageManager;
        this.spaceManager = spaceManager;
      }
    
      DateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy");
      DateFormat dayFormat = new SimpleDateFormat("EEEE");
      private static final String DAY_DATE_JSON = "dayDateJson";
      private static final String TEMPLATE = "templates/examplemacro.vm";
    
      @Override
      public BodyType getBodyType()
      {
         return BodyType.NONE;
      }
    
      @Override
      public OutputType getOutputType()
      {
         return OutputType.BLOCK;
      }
      @Override
      public String execute(Map<String, String> params, String body, ConversionContext conversionContext)
                              throws MacroExecutionException
      {
          // TODO: A lot more will go here
          return "Only a test";
      }
    }
    

    When a macro is invoked, Confluence calls its execute() method, so that's where we'll need to do the heavy lifting for our macro. Confluence passes parameters as strings in the params argument to execute. As implied by our variable declarations, our parameter will be a date. The class specifies the body type of NONE and an output type BLOCK for our macro. This tells Confluence what kind of content it should expect our macro to generate.

Step 6. Start up Confluence

We're not done yet, but let's start up Confluence and see what our macro does so far:

  1. Make sure you have saved all your code changes to this point.

  2. In a terminal window, navigate to the plugin root folder (where the pom.xml file is).

  3. Run the following command:

    1
    2
    atlas-run
    

    This command builds your plugin code, starts a Confluence instance, and installs your plugin. This may take a minute or two. When it's done, you will see many status lines on your screen concluding with something like this:

    1
    2
    [INFO] HOSTAPP started successfully in 71s at http://localhost:1990/confluence
    [INFO] Type CTRL-D to shutdown gracefully
    [INFO] Type CTRL-C to exit
    
  4. Open your browser and open Confluence at the address indicated in the output.

  5. At the login screen, enter the default credentials for an administrator user, admin/admin.

  6. Create a new page in the Demonstration Space by clicking the Create button from the header. 

  7. In the edit page screen, click Insert > Other Macros.

  8. Browsing to the macro in the Select Macro dialog. You can do so quickly by typing the first few letters of the macro name, such as Exa.

  9. Select the macro and insert it into your page.

As you saw in the preview pane and on the page after saving your changes, so far the macro generates our placeholder text, "Only a test". We'll make it do more next.

You can keep Confluence running at this point,  QuickReload will automatically redeploy any changes you build.

Step 7. Make your macro do more

Let's add the logic for generating our JSON-based calendar. We'll take this task piece by piece, so that we can examine and discuss each part of the code as we go.

  1. Open ExampleMacro.java for editing once again.

  2. Replace the body of the execute() method with this:

    1
    2
    @Override
    public String execute(Map<String, String> params, String body, ConversionContext conversionContext) 
                                                                         throws MacroExecutionException
    {
      Date startDate = getStartDateFromParams(params);
      JsonObject dayDateJsonObject = new JsonObject();
      JsonArray dayDateJsonArray = new JsonArray();
      for (int i=0; i < 7; i++)
      {
        Calendar cal = Calendar.getInstance();
        cal.setTime(startDate);
        cal.add(Calendar.DATE, i );
        Date date = cal.getTime();
        String day = dayFormat.format(date);
        dayDateJsonArray.add(nextDayDateJsonObject(day, date));
      }
      dayDateJsonObject.setProperty("daysdates", dayDateJsonArray);
    
      // TODO: Not done yet
       return "Only a test";
    }
    

    Our new execute() method now:

    • Initializes the startDate object by calling a new method, getStartDateFromParams(), passing it the params argument. It will be the job of getStartDateFromParams() to determine the start date and return it as a string into a Date object.

    • Instantiates a JsonObject and JsonArray instance.

    • Populates the array by taking the parameter as the starting date and looping seven times, each time incrementing the date by one. On each loop, the date is formatted by name and date, with the results stored in the JSON array (with the help of a method we'll create in a bit).

    • It sets the array as a daysdates property of the JSON object.

  3. Add the getStartDateFromParams() method called from execute() to the ExampleMacro class:

    1
    2
    private Date getStartDateFromParams(Map params) throws MacroExecutionException
    {
      Date startDate = new Date();
      if (params.size() > 1)
      {
        try
        {
          startDate = dateFormat.parse((String) params.get("date"));
        }
        catch (ParseException e)
        {
          throw new MacroExecutionException("Unable to parse date");
        }
      }
      return startDate;
    }
    

    The params argument always contains a RAW_PARAMS_KEY key containing the raw parameter string, so we'll only parse parameters when params.size is greater than 1. Also note that if the user doesn't provide a date, the startDate is initialized to the current date, which serves as our default.

  4. Add the other method we added to execute(), nextDayDateJsonObject() to the ExampleMacro class:

    1
    2
    private JsonObject nextDayDateJsonObject(String day, Date date)
    {
      JsonObject nextDayDateJsonObject = new JsonObject();
      nextDayDateJsonObject.setProperty("date",date);
      nextDayDateJsonObject.setProperty("day", day);
      return nextDayDateJsonObject;
    }
    

    This method simply populates a JSON object with the day and date for each cycle of the execute() loop. 

  5. So far, we stored the days and dates information as a JSON object. We still need to output the results. Replace the existing return statement (return "Only a test";) with these lines:

    1
    2
    VelocityContext contextMap = new VelocityContext(MacroUtils.defaultVelocityContext());
    contextMap.put(DAY_DATE_JSON, GeneralUtil.urlEncode(dayDateJsonObject.serialize()));
    return VelocityUtils.getRenderedTemplate(TEMPLATE, contextMap);
    

Notice our new return value. More realistically, you'd likely want to make your JSON available via REST, but REST is a topic for its own tutorial. To keep this simple, our plugin uses a Velocity template to render output as markup.

Notice the call to contextMap.put(). This function:

  1. Serializes the dayDateJsonObject JSON object into a string.
  2. URL encodes it.
  3. Puts it into a contextMap.

This is then rendered into a String containing markup according to the Velocity template we already created.

Step 8. Check your code

You macro plugin class is now complete. To review, our execute() method now:

  • Creates a JSON object containing two name/value pairs (one for the date and one for the day). This is what nextDayDateJsonObject does.
  • Calls nextDayDateJsonObject in the for loop of execute so that a JSON object is created for each of the seven dates.
  • Adds each of these objects to a JSON array named dayDateJsonArray.
  • Adds the JSON array filled with all seven JSON objects, as the value to the only property of another JSON object, dayDateJsonObject.

The end result being a single JSON object, dayDateJsonObject, that contains an array of seven JSON objects, each of which is a set of day and date name/value pairs.

You can check your completed class against the following:

Expand to view entire Java class  Expand source

1
2
package com.example.plugins.tutorial.confluence;

import java.util.Map;
import java.util.List;

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.pages.Page;
import com.atlassian.confluence.spaces.SpaceManager;
import com.atlassian.confluence.user.AuthenticatedUserThreadLocal;
import com.atlassian.user.User;
import com.opensymphony.util.TextUtils;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import com.atlassian.confluence.json.json.JsonArray;
import com.atlassian.confluence.json.json.JsonObject;
import com.atlassian.confluence.renderer.radeox.macros.MacroUtils;
import com.atlassian.confluence.util.GeneralUtil;
import com.atlassian.confluence.util.velocity.VelocityUtils;
import org.apache.velocity.VelocityContext;

public class ExampleMacro implements Macro
{
  private final PageManager pageManager;
  private final SpaceManager spaceManager;

  public ExampleMacro(PageManager pageManager, SpaceManager spaceManager)
  {
    this.pageManager = pageManager;
    this.spaceManager = spaceManager;
  }

  DateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy");
  DateFormat dayFormat = new SimpleDateFormat("EEEE");
  private static final String DAY_DATE_JSON = "dayDateJson";
  private static final String TEMPLATE = "templates/examplemacro.vm";

  @Override
  public BodyType getBodyType()
  {
    return BodyType.NONE;
  }
 
  @Override
  public OutputType getOutputType()
  {
    return OutputType.BLOCK;
  }

  @Override
  public String execute(Map<String, String> params, String body, ConversionContext conversionContext) 
                                                                       throws MacroExecutionException
  {
    Date startDate = getStartDateFromParams(params);
    JsonObject dayDateJsonObject = new JsonObject();
    JsonArray dayDateJsonArray = new JsonArray();

    for (int i=0; i < 7; i++)
    {
      Calendar cal = Calendar.getInstance();
      cal.setTime(startDate);
      cal.add(Calendar.DATE, i );
      Date date = cal.getTime();
      String day = dayFormat.format(date);
      dayDateJsonArray.add(nextDayDateJsonObject(day, date));
    }
    
    dayDateJsonObject.setProperty("daysdates", dayDateJsonArray);
    VelocityContext contextMap = new VelocityContext(MacroUtils.defaultVelocityContext());
    contextMap.put(DAY_DATE_JSON, GeneralUtil.urlEncode(dayDateJsonObject.serialize()));
    return VelocityUtils.getRenderedTemplate(TEMPLATE, contextMap);
  }
    
  private Date getStartDateFromParams(Map params) throws MacroExecutionException
  {
    Date startDate = new Date();
    if (params.size() > 1)
    {
      try
      {
      startDate = dateFormat.parse((String) params.get("date"));
      }
      catch (ParseException e)
      {
        throw new MacroExecutionException("Unable to parse date");
      }
    }
    return startDate;
  }

  private JsonObject nextDayDateJsonObject(String day, Date date)
  {
    JsonObject nextDayDateJsonObject = new JsonObject();
    nextDayDateJsonObject.setProperty("date",date);
    nextDayDateJsonObject.setProperty("day", day);
    return nextDayDateJsonObject;
  }
}

We're ready to try the macro again.

Step 9. Test the completed macro

To see your new macro in action:

  1. Make sure you have saved all your file changes.
  2. Rebuild your plugin and QuickReload will automatically reinstall it.
  3. Back in the Confluence browser window, try adding your macro again. This time, you should get the default calendar in the preview pane.
  4. Enter a date in the Date field using the dd/mm/yyyy format, such as 22/09/2014 for September 22, 2014.
  5. Click Insert.
  6. Click Save to view your macro output.

Congratulations, that's it

Have a chocolate!

Rate this page: