Last updated Apr 2, 2024

Upgrading and migrating an existing Confluence macro to 4.0

StatusLEGACY This tutorial applies to Confluence versions that have reached end of life.

See Preventing XSS issues with macros in Confluence 4.0 for information on how to prevent XSS issues with your plain-text macro.

Overview

This tutorial will cover how to upgrade an existing 3.x macro to a Confluence 4.0 macro, including migration.

The following concepts will be covered

  • The conditions that will lead to a 3.x macro getting migrated.
  • Having a 3.x and a 4.0 macro co-existing.
  • Using a custom migrator to migrate a macro.

Some Confluence plugin development knowledge is assumed.

Prerequisites

  1. A 3.x Confluence macro (or the one we will be using below).
  2. If you are using maven2 for your dependency management, then update the confluence.version in your pom to reflect Confluence 4.0:
1
2
<properties>
    <confluence.version>4.0</confluence.version>
    <confluence.data.version>3.5</confluence.data.version>
</properties>

Macros we will be migrating

For the purpose of this tutorial we will be migrating two Confluence 3.x macros (listed below):

Macro

Details

{mycheese}

This is our version of the {cheese} macro - it has no body and takes no parameters, the output is the string: "I really like cheese"

{mycolour}

This is our version of the {color} macro, it is bodied and takes a parameter (the colour) in the body of the macro. e.g. {mycolour}red:This is red text{mycolour}

Module Descriptors for these macros

These macros will be setup using the following atlassian-plugin.xml:

atlassian-plugin.xml

1
2
<macro key="mycheese"
       name="mycheese"
       class="com.atlassian.confluence.plugin.xhtml.MyCheeseMacro">
    <category name="development"/>
    <parameters/>
</macro>

<macro key="mycolour"
       name="mycolour"
       class="com.atlassian.confluence.plugin.xhtml.MyColourMacro">
    <category name="development"/>
    <parameters/>
</macro>

Macro Source

MyCheeseMacro.java

1
2
package com.atlassian.confluence.plugin.xhtml;

import com.atlassian.renderer.RenderContext;
import com.atlassian.renderer.v2.RenderMode;
import com.atlassian.renderer.v2.macro.BaseMacro;
import com.atlassian.renderer.v2.macro.MacroException;

import java.util.Map;

public class MyCheeseMacro extends BaseMacro
{
    @Override
    public boolean hasBody()
    {
        return false;
    }

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

    @Override
    public String execute(Map parameters, String body, RenderContext renderContext) throws MacroException
    {
        return "I <i>really</i> like cheese!";
    }
}

MyColourMacro.java

1
2
package com.atlassian.confluence.plugin.xhtml;

import com.atlassian.renderer.RenderContext;
import com.atlassian.renderer.v2.RenderMode;
import com.atlassian.renderer.v2.macro.BaseMacro;
import com.atlassian.renderer.v2.macro.MacroException;
import org.apache.commons.lang.StringUtils;

import java.text.MessageFormat;
import java.util.Map;

public class MyColourMacro extends BaseMacro
{
    public static final String COLOUR_PARAM = "colour";

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

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

    @Override
    public String execute(Map parameters, String body, RenderContext renderContext) throws MacroException
    {
        if (StringUtils.isBlank(body))
        {
            return "";
        }

        String[] bodyItems = StringUtils.split(body, ":", 2);
        if (bodyItems.length != 2)
        {
            return body;
        }

        return formatString(bodyItems[0], bodyItems[1]);
    }

    public String formatString(String colour, String body)
    {
        return MessageFormat.format("<span style=\"color: {0};\">{1}</span>", colour, body);
    }
}

Conditions for when a macro will be migrated

Now that we have the macros that we will be using for this tutorial covered, we will now cover the conditions for when a macro will get migrated:

  1. Does the macro have a wiki (2.x-3.x) and an XHTML (4.0) implementation available?
    1. If yes then we will migrate it either with the automatic migration or with a custom migrator.
  2. Does the macro have a body?
    1. If no then it will be migrated.
  3. Otherwise we will wrap it in the unmigrated-wiki-markup macro.

In order for the macro to show up in the Macro Browser, it will need to supply the correct metadata - this is for both 3.x and 4.0 macros. More information can be found here: Including Information in your Macro for the Macro Browser

This flow chart should make things a bit simpler:

What if a macro is not migrated?

If a macro is not migrated then the macro will not appear in the Macro Browser, nor will it appear in the autocomplete. Also if the macro is inserted through the Insert Wiki Markup dialog the macro will be wrapped with the unmigrated-wiki-markup macro.

Migrating our macros

Now if we look at the macros we have above, we can see that according to the flowchart the {mycheese} macro will get migrated as it does not have a body, however the {mycolour} macro will not, we will now cover what needs to be done to get the {mycolour} macro to migrate.

In order for us to get the mycolour macro to migrate we will need to provide an XHTML implementation of that macro and an appropriate module descriptor, we can implement the new Macro interface in the same macro class, which is what we will do here:

MyColourMacro.java (4.0)

1
2
package com.atlassian.confluence.plugin.xhtml;

import com.atlassian.confluence.content.render.xhtml.ConversionContext;
import com.atlassian.confluence.macro.Macro;
import com.atlassian.confluence.macro.MacroExecutionException;
import com.atlassian.renderer.RenderContext;
import com.atlassian.renderer.v2.RenderMode;
import com.atlassian.renderer.v2.macro.BaseMacro;
import com.atlassian.renderer.v2.macro.MacroException;
import org.apache.commons.lang.StringUtils;

import java.text.MessageFormat;
import java.util.Map;

public class MyColourMacro extends BaseMacro implements Macro
{
    public static final String COLOUR_PARAM = "colour";

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

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

    @Override
    public String execute(Map parameters, String body, RenderContext renderContext) throws MacroException
    {
        if (StringUtils.isBlank(body))
        {
            return "";
        }

        String[] bodyItems = StringUtils.split(body, ":", 2);
        if (bodyItems.length != 2)
        {
            return body;
        }

        return formatString(bodyItems[0], bodyItems[1]);
    }

    public String formatString(String colour, String body)
    {
        return MessageFormat.format("<span style=\"color: {0};\">{1}</span>", colour, body);
    }

    @Override
    public String execute(Map<String, String> params, String body, ConversionContext conversionContext) throws MacroExecutionException
    {
        try
        {
            return execute(params, body, (RenderContext) null);
        }
        catch (MacroException e)
        {
            throw new MacroExecutionException(e);
        }
    }

    @Override
    public BodyType getBodyType()
    {
        return BodyType.PLAIN_TEXT;
    }

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

As you can see the new execute(...) method delegates to the old one, we are using the same functionality as the 3.x macro for our 4.0 macro.

Once the macro is implemented we need to specify a new module descriptor for it - xhtml-macro.

atlassian-plugin.xml

1
2
<xhtml-macro key="mycolour-xhtml"
             name="mycolour"
             class="com.atlassian.confluence.plugin.xhtml.MyColourMacro">
    <category name="development"/>
    <parameters/>
</xhtml-macro>

This looks much the same as the 3.x macro at the moment, the only difference is the new module descriptor name: xhtml-macro. For the migration to work, just the macro name has to match. The classes are allowed to be different (and usually are).

Now that we have the XHTML implementation of it we will be able to see it in the Macro Browser and in autocomplete, it also means that the macro will have it's own placeholder rather than the unmigrated-wiki-markup placeholder.

Custom migrators

A custom migrator can be specified by a plugin in order to migrate a specified macro. To do this one must first implement the Migrator interface and then define the migrator as a module in the atlassian-plugin.xml.

MacroMigration.java

1
2
public interface MacroMigration
{
    /**
     * Migrates a wiki-markup representation of a macro to XHTML
     * @param macro The {@link com.atlassian.confluence.xhtml.api.MacroDefinition} is wiki-markup form.
     * @param context The {@link com.atlassian.confluence.content.render.xhtml.ConversionContext} to perform the migration under.
     * @return An XHTML representation of the macro.
     */
    MacroDefinition migrate(MacroDefinition macro, ConversionContext context);
}

Using a custom migrator to remove the parameter from our mycolour macro.

In this section we will implement a migrator to remove the parameter from the body of our mycolour macro and insert it as a parameter, this will occur whenever a wiki-markup version of this macro is encountered (either at initial migration time or by using the Insert Wiki Markup dialog).

In order to do this we will first update the execute(...) method of our macro to take a parameter:

New execute(...) method for parameters)

1
2
@Override
public String execute(Map<String, String> params, String body, ConversionContext conversionContext) throws MacroExecutionException
{
    if (!params.containsKey(COLOUR_PARAM))
    {
        return body;
    }

    String colour = params.get(COLOUR_PARAM);
    return formatString(colour, body);
}

We will also update the module descriptor for this macro in order to support parameters in the Macro Browser.

New xhtml-macro module descriptor with parameter information

1
2
<xhtml-macro key="mycolour-xhtml"
             name="mycolour"
             class="com.atlassian.confluence.plugin.xhtml.MyColourMacro">
    <category name="development"/>
    <parameters>
        <parameter name="colour" type="enum">
            <value name="red"/>
            <value name="green"/>
            <value name="blue"/>
            <value name="pink"/>
            <value name="black"/>
        </parameter>
    </parameters>
</xhtml-macro>

Now that the macro is setup to accept a parameter we will implement the Migrator interface, as you can see the migrator uses simular logic (to the 3.x macro) to extract the parameter and insert it into the macro definition. The MacroDefinition returned from this method will replace the one read in.

MyColourMacroMigrator.java

1
2
package com.atlassian.confluence.plugin.xhtml;

import com.atlassian.confluence.content.render.xhtml.ConversionContext;
import com.atlassian.confluence.content.render.xhtml.definition.MacroBody;
import com.atlassian.confluence.content.render.xhtml.definition.PlainTextMacroBody;
import com.atlassian.confluence.macro.xhtml.MacroMigration;
import com.atlassian.confluence.xhtml.api.MacroDefinition;
import org.apache.commons.lang.StringUtils;

import java.util.HashMap;
import java.util.Map;

public class MyColourMacroMigrator implements MacroMigration
{
    @Override
    public MacroDefinition migrate(MacroDefinition macroDefinition, ConversionContext conversionContext)
    {
        MacroBody macroBody = macroDefinition.getBody();
        if (StringUtils.isBlank(macroBody.getBody()))
        {
            return macroDefinition;
        }

        final String[] bodyItems = StringUtils.split(macroBody.getBody(), ":", 2);
        if (bodyItems.length != 2)
        {
            return macroDefinition;
        }

        Map<String, String> params = new HashMap<String, String>(1)
        {{
            put(MyColourMacro.COLOUR_PARAM, bodyItems[0]);
        }};
        macroDefinition.setParameters(params);

        MacroBody newBody = new PlainTextMacroBody(bodyItems[1]);
        macroDefinition.setBody(newBody);

        return macroDefinition;
    }
}

Now that we have the Migrator defined we will need to define the module in the atlassian-plugin.xml file, the macro-migrator module descriptor takes three parameter; the key, the macro-name and the class:

atlassian-plugin.xml macro-migrator module descriptor

1
2
<macro-migrator key="mycolour-migrator"
                macro-name="mycolour"
                class="com.atlassian.confluence.plugin.xhtml.MyColourMacroMigrator"/>

Macro Aliases

You might want to consider simplifying your plugin for Confluence 4.0 by removing any macro aliases.

Just as a quick recap, it is possible to declare an alias for your macro by adding a duplicate macro declaration like so:

1
2
<macro name="blogs" key="blogs-key" class="com.example.BlogsMacro">
...
</macro>
<macro name="posts" key="posts-key" class="com.example.BlogsMacro">
...
</macro>

This allowed users to use the macro by entering either {blogs} or {posts} in wiki markup.

If you would like to migrate all occurrences of the alias to the original macro (i.e. {posts} to {blogs}), when a user upgrades to 4.0, you can do so by adding a macro-migrator to your plugin descriptor:

1
2
<macro-migrator key="posts-migrator" macro-name="posts" class="com.example.PostsMacroMigrator"/>

Of course, you will have to write the com.example.PostsMacroMigrator that does the renaming.

Conclusion

In this tutorial you saw how macro migration will occur for 3.x macros, how to implement a 4.0 macro and have it co-exist with a 3.x macro and how to implement a custom macro migrator.

Rate this page: