The goal is to build a small macro plugin deployable via the Confluence plugin API rendering some spaces.

Please note that I am going to build the plugin using just Maven and not the Atlassian Maven Wrapper called the “Atlassian Plugin SDK” – more information about that is available at the Atlassian website.

The macro output will be rendered using a Velocity template and all messages are stored for i18n in properties files bundled with the plugin.

If you need to set up an instance of Confluence first, head over to this article.

Confluence Macro Tutorial Components
Figure 1. Confluence_Macro_Tutorial_Components

Steps

  • Create a new project structure using archetypes

    mvn archetype:generate -DarchetypeCatalog=http://svn.atlassian.com/svn/public/atlassian/maven-plugins/archetype-catalog
  • This should show some output like this:

    Choose archetype:
    1: http://svn.atlassian.com/svn/public/atlassian/maven-plugins/archetype-catalog -> bamboo-plugin-archetype (Archetype for a new Atlassian Bamboo plugin project)
    2: http://svn.atlassian.com/svn/public/atlassian/maven-plugins/archetype-catalog -> confluence-plugin-archetype (Archetype for a new Atlassian Confluence plugin project)
    3: http://svn.atlassian.com/svn/public/atlassian/maven-plugins/archetype-catalog -> confluence-plugin2-archetype (Archetype for a new Atlassian Confluence plugin 2 project (OSGi-based plugin))
    4: http://svn.atlassian.com/svn/public/atlassian/maven-plugins/archetype-catalog -> crowd-plugin-archetype (Archetype for a new Atlassian Crowd plugin project)
    5: http://svn.atlassian.com/svn/public/atlassian/maven-plugins/archetype-catalog -> crucible-plugin-archetype (Archetype for a new Atlassian Fisheye or Atlassian Crucible plugin project)
    6: http://svn.atlassian.com/svn/public/atlassian/maven-plugins/archetype-catalog -> jira-plugin-archetype (Archetype for a new Atlassian JIRA plugin project)
    Choose a number:  (1/2/3/4/5/6):
  • Be sure to select confluence-plugin2-archetype as we want to build an OSGi plugin for Confluence!

  • Enter the groupId, artifactId, version and optional the package name and confirm your selection

    Define value for groupId: : com.hascode.confluence
    Define value for artifactId: : macro-tutorial
    Define value for version:  1.0-SNAPSHOT: : 0.1
    Define value for package:  com.hascode.confluence: :
    Confirm properties configuration:
    groupId: com.hascode.confluence
    artifactId: macro-tutorial
    version: 0.1
    package: com.hascode.confluence
    Y: :
  • Edit the project name in the pom.xml and adjust the Confluence version used – the file should look like this:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    
     <parent>
     <groupId>com.atlassian.confluence.plugin.base</groupId>
     <artifactId>confluence-plugin-base</artifactId>
     <version>25</version>
     </parent>
    
     <modelVersion>4.0.0</modelVersion>
     <groupId>com.hascode.confluence</groupId>
     <artifactId>macro-tutorial</artifactId>
     <version>0.1</version>
    
     <name>hasCode.com - Confluence Macro Tutorial</name>
     <packaging>atlassian-plugin</packaging>
    
     <properties>
     <atlassian.plugin.key>com.hascode.confluence.macro-tutorial</atlassian.plugin.key>
     <!-- Confluence version -->
     <atlassian.product.version>2.9</atlassian.product.version>
     <!-- Confluence plugin functional test library version -->
     <atlassian.product.test-lib.version>1.4.1</atlassian.product.test-lib.version>
     <!-- Confluence data version -->
     <atlassian.product.data.version>2.9</atlassian.product.data.version>
     </properties>
    </project>
  • Now edit the plugin deployment descriptor named atlassian-plugin.xml in src/main/resources as described in the Macro Module Documentation and add a reference to the properties file used for i18n

    <atlassian-plugin key="${atlassian.plugin.key}" name="hasCode.com Macro Tutorial" pluginsVersion="2">
     <resource type="i18n" name="i18n" location="com.hascode.confluence.macro.messages"/>
     <plugin-info>
     <description key="hascode.plugin.description"/>
     <version>${project.version}</version>
     <vendor name="hasCode.com" url="https://www.hascode.com"/>
     </plugin-info>
    
     <macro name="DemoSpacesMacro" key="hascode.plugin.macro.name" class="com.hascode.confluence.macro.SpacesMacro"/>
    </atlassian-plugin>
  • Now create the directory structure for the property files and create a file named messages.properties in the created directory src/main/resources/com/hascode/confluence/macro

    mkdir-p src/main/resources/com/hascode/confluence/macro
  • Add the two keys needed to the messages.properties

    hascode.plugin.description=This is a plugin descriptionhascode.plugin.macro.name=Spaces Overview Macro
  • Add a package named com.hascode.confluence.macro and add a class SpacesMacro

    package com.hascode.confluence.macro;
    
    import java.util.Map;
    
    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;
    
    public class SpacesMacro extends BaseMacro
            implements
                com.atlassian.renderer.v2.macro.Macro {
    
        /*
         * (non-Javadoc)
         *
         * @see com.atlassian.renderer.v2.macro.Macro#execute(java.util.Map,
         * java.lang.String, com.atlassian.renderer.RenderContext)
         */
        public String execute(Map params, String body, RenderContext renderContext)
                throws MacroException {
            return "TEST";
        }
    
        /*
         * (non-Javadoc)
         *
         * @see com.atlassian.renderer.v2.macro.Macro#getBodyRenderMode()
         */
        public RenderMode getBodyRenderMode() {
            return RenderMode.ALL;
        }
    
        /*
         * (non-Javadoc)
         *
         * @see com.atlassian.renderer.v2.macro.Macro#hasBody()
         */
        public boolean hasBody() {
            return false;
        }
    
        /*
         * (non-Javadoc)
         *
         * @see com.atlassian.renderer.v2.macro.Macro#isInline()
         */
        public boolean isInline() {
            return false;
        }
    
        /*
         * (non-Javadoc)
         *
         * @see
         * com.atlassian.renderer.v2.macro.Macro#suppressMacroRenderingDuringWysiwyg
         * ()
         */
        public boolean suppressMacroRenderingDuringWysiwyg() {
            return false;
        }
    
        /*
         * (non-Javadoc)
         *
         * @seecom.atlassian.renderer.v2.macro.Macro#
         * suppressSurroundingTagDuringWysiwygRendering()
         */
        public boolean suppressSurroundingTagDuringWysiwygRendering() {
            return false;
        }
    
    }
  • This macro returns a simple string so now add templating, logging and functionality

  • First create a directory to put the template into in src/main/resources

    mkdir-p src/main/resources/template
  • Create a template named macro.vm in src/main/resources/template

    Spaces Macro
    $output
  • Change the class SpacesMacro to this

    package com.hascode.confluence.macro;
    
    import java.util.Map;
    
    import com.atlassian.confluence.renderer.radeox.macros.MacroUtils;
    import com.atlassian.confluence.util.velocity.VelocityUtils;
    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;
    
    public class SpacesMacro extends BaseMacro
            implements
                com.atlassian.renderer.v2.macro.Macro {
    
        private static final String    MACRO_TEMPLATE    = "template/macro.vm";
    
        /*
         * (non-Javadoc)
         *
         * @see com.atlassian.renderer.v2.macro.Macro#execute(java.util.Map,
         * java.lang.String, com.atlassian.renderer.RenderContext)
         */
        public String execute(Map params, String body, RenderContext renderContext)
                throws MacroException {
            Map ctx = MacroUtils.defaultVelocityContext();
            ctx.put("output", "Test");
            return VelocityUtils.getRenderedTemplate(MACRO_TEMPLATE, ctx);
        }
    
        [..]
    }
  • Now add some logging:

    package com.hascode.confluence.macro;
    
    import java.util.Map;
    
    import org.apache.log4j.Logger;
    [..]
    
    public class SpacesMacro extends BaseMacro
            implements
                com.atlassian.renderer.v2.macro.Macro {
    
        public static final String    LOGGER_KEY        = "com.hascode.confluence.macro";
        private static final String    MACRO_TEMPLATE    = "template/macro.vm";
        private static final Logger    log                = Logger.getLogger(LOGGER_KEY);
    
        /*
         * (non-Javadoc)
         *
         * @see com.atlassian.renderer.v2.macro.Macro#execute(java.util.Map,
         * java.lang.String, com.atlassian.renderer.RenderContext)
         */
        public String execute(Map params, String body, RenderContext renderContext)
                throws MacroException {
            log.debug("macro execution triggered with params " + params.toString());
            Map ctx = MacroUtils.defaultVelocityContext();
            ctx.put("output", "Test");
            return VelocityUtils.getRenderedTemplate(MACRO_TEMPLATE, ctx);
        }
    }
  • Now enable logging in your Confluence installation .. go Browse > Confluence Admin > Logging and Profiling and add a new logging entry with the key specified in LOGGER_KEY, com.hascode.confluence.macro and your desired log level

  • After the plugin deployment and eventually a server restart you might enjoy your logging in <confluence-installation>/logs/catalina.out (the location might differ)

  • Now adding some functionality the macro class finally looks like this:

    package com.hascode.confluence.macro;
    
    import java.util.List;
    import java.util.Map;
    
    import org.apache.log4j.Logger;
    
    import com.atlassian.confluence.renderer.radeox.macros.MacroUtils;
    import com.atlassian.confluence.spaces.Space;
    import com.atlassian.confluence.spaces.SpaceManager;
    import com.atlassian.confluence.util.velocity.VelocityUtils;
    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;
    
    public class SpacesMacro extends BaseMacro
            implements
                com.atlassian.renderer.v2.macro.Macro {
    
        public static final String    LOGGER_KEY        = "com.hascode.confluence.macro";
        private static final String    MACRO_TEMPLATE    = "template/macro.vm";
        private static final Logger    log                = Logger.getLogger(LOGGER_KEY);
    
        private SpaceManager        spaceManager;
    
        /*
         * (non-Javadoc)
         *
         * @see com.atlassian.renderer.v2.macro.Macro#execute(java.util.Map,
         * java.lang.String, com.atlassian.renderer.RenderContext)
         */
        public String execute(Map params, String body, RenderContext renderContext)
                throws MacroException {
            log.debug("macro execution triggered with params " + params.toString());
            Map ctx = MacroUtils.defaultVelocityContext();
            ctx.put("spaces", this.findSpaces());
            return VelocityUtils.getRenderedTemplate(MACRO_TEMPLATE, ctx);
        }
        /*
         * (non-Javadoc)
         *
         * @see com.atlassian.renderer.v2.macro.Macro#getBodyRenderMode()
         */
        public RenderMode getBodyRenderMode() {
            return RenderMode.ALL;
        }
    
        /*
         * (non-Javadoc)
         *
         * @see com.atlassian.renderer.v2.macro.Macro#hasBody()
         */
        public boolean hasBody() {
            return false;
        }
    
        /*
         * (non-Javadoc)
         *
         * @see com.atlassian.renderer.v2.macro.Macro#isInline()
         */
        public boolean isInline() {
            return false;
        }
    
        /*
         * (non-Javadoc)
         *
         * @see
         * com.atlassian.renderer.v2.macro.Macro#suppressMacroRenderingDuringWysiwyg
         * ()
         */
        public boolean suppressMacroRenderingDuringWysiwyg() {
            return false;
        }
    
        /*
         * (non-Javadoc)
         *
         * @seecom.atlassian.renderer.v2.macro.Macro#
         * suppressSurroundingTagDuringWysiwygRendering()
         */
        public boolean suppressSurroundingTagDuringWysiwygRendering() {
            return false;
        }
    
        @SuppressWarnings("unchecked")
        private List findSpaces() {
            return spaceManager.getAllSpaces();
        }
    
        /**
         * set by IoC container
         *
         * @param spaceManager
         *            the space manager
         */
        public void setSpaceManager(SpaceManager spaceManager) {
            this.spaceManager = spaceManager;
        }
    }
  • What happens here – the spaceManager is set by dependency injection via Spring (setter-injection) – the spaceManager is used to pull a (ugly,nongeneric) list of spaces. this list is passed to the velocity template

  • Non adjust the velocity template macro.vm to this:

    Spaces Macro<br/>
    #foreach ($space in $spaces)
    - $space.getName()<br/>
    #end
  • Now build and deploy via the confluence administration area, just upload target/macro-tutorial-0.1.jar in the plugin section :

    mvn package
  • A faster method to build and deploy the plugin without using the file upload is to use the following goal:

    mvn atlassian-pdk:install
  • To make this work, add the following entry to your ~/.m2/settings.xml

    <settings>
     [..]
     <profiles>
     <profile>
     <properties>
     <downloadSources>true</downloadSources>
     <downloadJavadocs>true</downloadJavadocs>
     <atlassian.pdk.server.url>http://localhost:8080/</atlassian.pdk.server.url>
     <atlassian.pdk.server.username>youradminusername</atlassian.pdk.server.username>
     <atlassian.pdk.server.password>youradminpassword</atlassian.pdk.server.password>
     </properties>
     </profile>
     [..]
     </profiles>
     [..]
  • Another alternative if you do not have a running installation of confluence, use the following command to get Confluence via Maven and deploy hte plugin in one step – the URL is http://localhost:1990/confluence

    mvn -Pplugin-debug
  • Head over to the plugin section in your confluence and check if the plugin deployment was successful

  • You should now be able to use the Macro in a Confluence Page

    {DemoSpacesMacro}
  • Here some screenshots of the deployed plugin in the Confluence administration area and the rendered macro in a page in my testing environment with three existing spaces

image
Figure 2. confluence-plugin-administration

Sources Download

  • You’re able to download the sources from GitHub.org or if you have Mercurial installed just checkout like this

    hg clone http://github.com/hascode/hascode-tutorials

Troubleshooting

  • Unable to find resource ‘tangosol-coherence:coherence:jar:3.3 Oracle removed the libraries for the Tangosol/Coherence Cache from the Maven repositories so if you’re not going to use these directly just add this exclude rule to your pom.xml

    <dependency>
         <groupId>com.atlassian.confluence</groupId>
         <artifactId>confluence</artifactId>
         <version>${confluence.version}</version>
         <scope>provided</scope>
         <exclusions>
             <exclusion>
                 <groupId>tangosol-coherence</groupId>
                 <artifactId>coherence</artifactId>
             </exclusion>
             <exclusion>
                 <groupId>tangosol-coherence</groupId>
                 <artifactId>tangosol</artifactId>
             </exclusion>
         </exclusions>
    </dependency>
  • “Unable to find resource seraph ..” - add another exclusion to your pom.xml

    <dependency>
         <groupId>com.atlassian.confluence</groupId>
         <artifactId>confluence</artifactId>
         <version>${confluence.version}</version>
         <scope>provided</scope>
         <exclusions>
             <exclusion>
                 <groupId>seraph</groupId>
                 <artifactId>seraph</artifactId>
             </exclusion>
         </exclusions>
    </dependency>
  • “The plugin ‘org.twdata.maven:maven-cli-plugin’ does not exist or no valid version could be found” or other dependencies not resolvable.. – add Atlassian’s Maven repositories to your pom.xml

    <repositories>
      <repository>
        <id>atlassian-public</id>
        <url>https://maven.atlassian.com/repository/public</url>
        <snapshots>
          <enabled>true</enabled>
        </snapshots>
        <releases>
          <enabled>true</enabled>
        </releases>
      </repository>
      <repository>
        <id>atlassian-m1-repository</id>
        <url>https://maven.atlassian.com/maven1</url>
        <layout>legacy</layout>
      </repository>
      <repository>
        <id>atlassian-unknown</id>
        <name>atlassian-unknown</name>
        <url>http://repository.atlassian.com/</url>
      </repository>
    </repositories>
    <pluginRepositories>
      <pluginRepository>
        <id>atlassian-public</id>
        <url>https://maven.atlassian.com/repository/public</url>
        <snapshots>
          <enabled>true</enabled>
        </snapshots>
        <releases>
          <enabled>true</enabled>
        </releases>
      </pluginRepository>

Article Updates

  • 2015-03-03: Table of contents added.