How to build a Confluence Macro Plugin

April 13th, 2010 by

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.

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="http://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 description
    hascode.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

Sources Download

  • You’re able to download the sources from Bitbucket.org or if you have Mercurial installed just checkout like this
    hg clone http://bitbucket.org/hascode/hascode-tutorials

Troubleshooting

  • Unable to find resource ‘tangosol-coherence:coherence:jar:3.3Oracle 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>

Resources

Tags: , , , , , ,

10 Responses to “How to build a Confluence Macro Plugin”

  1. hotta Says:

    This is really useful. Any idean how to hook up the licensing module with the confluence plug-in?
    thanks,
    Hotts

  2. mk Says:

    You could take a look at the Confluence LicenseManager class its a good starting point to get license information and to get to the LicenseRegistry.

    There are also some helping methods available in the velocity context in generalUtil and action to check license details from the Confluence License.

    Also there is a nice License Creator Plugin from Customware.net but I haven’t tested it yet -> http://www.customware.net/repository/display/AtlassianPlugins/Creating+a+Licensed+Plugin

    Finally you could take a look at this article: http://www.hascode.com/2010/05/snippets-getting-license-information-from-the-confluence-api/

  3. kshitij vyas Says:

    hi..this was really useful..but is there any other way to built your macro.I mean without using maven.can’t we simply use java and xml.? If there is nay way then please tell me the procedure.

  4. micha kops Says:

    It is possible to create the macro without using maven but that means searching all referenced libraries by hand .. no automatic deployment process .. no versioning of your dependencies .. no project archetypes .. no embedded confluence developer instance ..

    of course you might compile everything without maven and package it into a jar .. everything in src/main/resources goes into the jar’s root, the compiled class files into /com/package/Yourclass.class etc .. but I highly recommend to use Maven.

    Perhaps you’re more comfortable with Atlassian’s Plugin SDK .. it’s a wrapper for Maven and allows you to create, build and deploy your plugins using customized commands as atlas-create-confluence-plugin

    There is some detailed information on the Plugin SDK at Atlassian’s Documentation Wiki

  5. kshitij vyas Says:

    thanks micha :: i have built without using maven but the problem is that when i upload my .jar file into confluence. Its is showing an error like:-Could not find a handler capable of installing the plugin at ag-supporrt.jar. Check that the file is a valid plugin.
    Is there some i am missing?
    1.class file.
    2. xml file.
    3 velocity file.
    i am not sure that i am going in a right way.please ellaborate more if you can.
    coz i am not being able to find any thing from internet…
    with regards–kshitij

  6. micha kops Says:

    Seems like the Maven plugin creates some additional information in META-INF:

    $ jar -tvf macro-tutorial-0.1.jar
    0 Fri Dec 03 08:10:02 CET 2010 META-INF/
    124 Fri Dec 03 08:10:00 CET 2010 META-INF/MANIFEST.MF
    0 Fri Dec 03 08:09:46 CET 2010 com/
    0 Fri Dec 03 08:09:46 CET 2010 com/hascode/
    0 Fri Dec 03 08:09:46 CET 2010 com/hascode/confluence/
    0 Fri Dec 03 08:09:58 CET 2010 com/hascode/confluence/macro/
    0 Fri Dec 03 08:09:46 CET 2010 template/
    104 Fri Dec 03 08:09:46 CET 2010 com/hascode/confluence/macro/messages.properties
    2798 Fri Dec 03 08:09:58 CET 2010 com/hascode/confluence/macro/SpacesMacro.class
    75 Fri Dec 03 08:09:46 CET 2010 template/macro.vm
    452 Fri Dec 03 08:09:46 CET 2010 atlassian-plugin.xml
    0 Fri Dec 03 08:10:02 CET 2010 META-INF/maven/
    0 Fri Dec 03 08:10:02 CET 2010 META-INF/maven/com.hascode.confluence/
    0 Fri Dec 03 08:10:02 CET 2010 META-INF/maven/com.hascode.confluence/macro-tutorial/
    2306 Fri Dec 03 07:56:26 CET 2010 META-INF/maven/com.hascode.confluence/macro-tutorial/pom.xml
    119 Fri Dec 03 08:10:02 CET 2010 META-INF/maven/com.hascode.confluence/macro-tutorial/pom.properties

    I have put the sources for this tutorial on Bitbucket and have also uploaded a built plugin-jar-file here for you – you could compare this one to your approach

  7. john williams Says:

    Thanks a lot – I got it running without using Maven. I’m going to try it with Maven the next time – it seems to work easier with it ..

  8. Maik Kempe Says:

    Hey Micha,

    the URI for the archetypes changed, its now ‘http://svn.atlassian.com/svn/public/atlassian/maven-plugins/archetype-catalog’.

  9. micha kops Says:

    thanks mate! does the new archetype work only with atlassian’s plugin sdk or does it work also with a normal working maven instance?

  10. Maik Kempe Says:

    Mmmmm, i don’t know, get: ‘* or one of its dependencies could not be resolved’. I like it…

Leave a Reply

Please leave these two fields as-is:

Protected by Invisible Defender. Showed 403 to 80,962 bad guys.

Search
Categories