Status | LEGACY 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.
This tutorial will cover how to upgrade an existing 3.x
macro to a Confluence 4.0 macro, including migration.
3.x
macro getting migrated.3.x
and a 4.0
macro co-existing.3.x
Confluence macro (or the one we will be using below).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>
For the purpose of this tutorial we will be migrating two Confluence 3.x
macros (listed below):
Macro | Details |
---|---|
{ | This is our version of the { |
{ | This is our version of the { |
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>
MyCheeseMacro.java
1 2package 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 2package 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); } }
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:
2.x-3.x
) and an XHTML (4.0
) implementation available?
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:
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.
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 2package 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.
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 2public 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); }
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 2package 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.
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: