Writing a Macro

Last modified by Alex Cotiugă on 2024/01/17 17:28

This tutorial explains how to create an XWiki Rendering Macro in Java. See the Macro Transformation reference documentation for additional details.

Pre-requisites:

General Principles

Start by understanding the Macro execution process.

In order to implement a new Macro you'll need to write 2 classes:

  • One that is a pure Java Bean and that represents the parameters allowed for that macro, including mandatory parameters, default values, parameter descriptions. An instance of this class will be automagically populated when the user calls the macro in wiki syntax.
  • Another one that is the Macro itself. This class should implement the Macro interface. However we recommend extending AbstractMacro which does some heavy lifting for you. By doing so you'll only have to implement 2 methods:
        /**
         * @return true if the macro can be inserted in some existing content such as a paragraph, a list item etc. For
         *         example if I have <code>== hello {{velocity}}world{{/velocity}}</code> then the Velocity macro must
         *         support the inline mode and not generate a paragraph.
         */

       boolean supportsInlineMode();

       /**
         * Executes the macro.
         *
         * @param parameters the macro parameters in the form of a bean defined by the {@link Macro} implementation
         * @param content the content of the macro
         * @param context the context of the macros transformation process
         * @return the result of the macro execution as a list of Block elements
         * @throws MacroExecutionException error when executing the macro
         */

        List<Block> execute(P parameters, String content, MacroTransformationContext context)
           throws MacroExecutionException;

Then you'll need to register your Macro class with the Component Manager so that it can be called from a wiki page. You register with a Macro.class role and a hint corresponding to the macro name. For example if you've registered your macro under the mymacro hint, you'll be able to call:

{{mymacro .../}}

In addition you can register your Macro for all syntaxes or only for a given syntax. In order to register only for a given syntax you must use a hint in the format macroname/syntaxid. For example: mymacro/xwiki/2.0.

Security considerations

The Macro Transformation Context can be set in "Restricted Mode". When set, this parameter indicates that rendering is performed in a context where:

  • modifications to the database or other privileged operations should not be performed
  • expensive computations should not be performed
  • any potentially dangerous operations should not be performed (like executing scripts)

Your macro should respect this parameter (by checking macroContext.getTransformationContext().isRestricted()) and either not execute at all or execute in a restricted mode if the above is a concern for your macro.

Macro preparation

15.9+ 

Macros are sometimes called to prepare a MacroBlock. The idea is to pre process everything which is not impacted by the runtime context and to cache the result in an attribute of the MacroBlock so that each #execute call can reuse it instead of redoing the work in each execution. It can have a very important impact on performance for macros in which most of the work is not impacted by the runtime context.

For that you need to implement the following method:


    /**
     * Prepare a {@link MacroBlock} meant to be cached to be executed several times. The goal is to pre-execute
     * everything that is independent of any context and store it in an annotation of the passed {@link MacroBlock}.
     * <p>
     * The result of the pre-execution is generally stored in the {@link MacroBlock} as attribute. Since the prepared
     * block might end up exposed in an unsafe environment the value should be either clonable or immutable.
     *
     * @param macroBlock the macro block to prepare
     */

   default void prepare(MacroBlock macroBlock) throws MacroPreparationException

Implementing a Macro

Here are detailed steps explaining how you can create a macro and deploy it.

Creating a Macro using Maven

In order for this job to go as smooth as possible we have created a Maven Archetype to help you create a simple macro module with a single command.

After you've installed Maven, open a shell prompt an type:
mvn archetype:generate

This will list all archetypes available on Maven Central. If instead you wish to directly use the XWiki Rendering Macro Archetype, you can directly type (update the version to use the version you wish to use):

mvn archetype:generate \
  -DarchetypeArtifactId=xwiki-rendering-archetype-macro \
  -DarchetypeGroupId=org.xwiki.rendering \
  -DarchetypeVersion=3.2

Then follow the instructions. For example:

vmassol@tmp $ mvn archetype:generate
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] >>> maven-archetype-plugin:2.0:generate (default-cli) @ standalone-pom >>>
[INFO]
[INFO] <<< maven-archetype-plugin:2.0:generate (default-cli) @ standalone-pom <<<
[INFO]
[INFO] --- maven-archetype-plugin:2.0:generate (default-cli) @ standalone-pom ---
[INFO] Generating project in Interactive mode
[INFO] No archetype defined. Using maven-archetype-quickstart (org.apache.maven.archetypes:maven-archetype-quickstart:1.0)
Choose archetype:
...
466: remote -> org.xwiki.rendering:xwiki-rendering-archetype-macro (Make it easy to create a maven project for creating XWiki Rendering Macros.)
...
Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): 143: 466
Choose version:
1: 3.2-milestone-3
2: 3.2-rc-1
3: 3.2
Choose a number: 3:
Define value for property 'groupId': : com.acme
Define value for property 'artifactId': : example
Define value for property 'version':  1.0-SNAPSHOT: :
Define value for property 'package':  com.acme: :
Confirm properties configuration:
groupId: com.acme
artifactId: example
version: 1.0-SNAPSHOT
package: com.acme
 Y: : Y
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 12.248s
[INFO] Finished at: Fri Mar 11 14:54:46 CET 2011
[INFO] Final Memory: 7M/81M
[INFO] ------------------------------------------------------------------------

Then go in the created directory (example in our example above) and run mvn install to build your macro.

If you are a XWiki developer or you just want to see more examples of implemented macros, you can check the source code for the Macros in our SCM.

Now, let's take a moment and examine the newly generated project. Navigating in the project's folder, we will see the following structure:

  • pom.xml - the project's POM file.
  • src/main/java/.../ExampleMacroParameters.java - a simple bean representing the Macro's parameters. This bean contains annotations to tell the Macro engine the parameter that are mandatory, the list of allowed values, parameter descriptions, etc.
  • src/main/java/.../internal/ExampleMacro.java - the macro itself.
  • src/main/resources/META-INF/components.txt - the list of component implementations. Since our Macro is a component it needs to be listed there. Each component must have its full name written on a separate line (e.g. com.acme.internal.ExampleMacro).
  • src/test/java/.../IntegrationTests.java - JUnit Test Suite to run rendering tests for the Macro.
  • src/test/resources/example1.test - a test file for testing the Macro. It tests that the macro works when standalone.
  • src/test/resources/example2.test - a test file for testing the Macro. It tests that the macro works when inline.

Macro Code

Here's the content of our generated ExampleMacro.java.

package com.acme.internal;

import javax.inject.Named;

import java.util.List;

import org.xwiki.component.annotation.Component;
import org.xwiki.rendering.block.Block;
import org.xwiki.rendering.block.WordBlock;
import org.xwiki.rendering.block.ParagraphBlock;
import org.xwiki.rendering.macro.AbstractMacro;
import org.xwiki.rendering.macro.MacroExecutionException;
import com.acme.ExampleMacroParameters;
import org.xwiki.rendering.transformation.MacroTransformationContext;

/**
 * Example Macro.
 */

@Component
@Named("simple-example")
public class ExampleMacro extends AbstractMacro<ExampleMacroParameters>
{
   /**
     * The description of the macro.
     */

   private static final String DESCRIPTION = "Simple Example Macro";

   /**
     * Create and initialize the descriptor of the macro.
     */

   public ExampleMacro()
   {
       super("Simple Example", DESCRIPTION, ExampleMacroParameters.class);
   }

   @Override
   public List<Block> execute(ExampleMacroParameters parameters, String content, MacroTransformationContext context)
       throws MacroExecutionException
   {
        List<Block> result;

        List<Block> wordBlockAsList = List.of(new WordBlock(parameters.getParameter()));

       // Handle both inline mode and standalone mode.
       if (context.isInline()) {
            result = wordBlockAsList;
       } else {
           // Wrap the result in a Paragraph Block since a WordBlock is an inline element, and it needs to be
           // inside a standalone block.
           result = List.of(new ParagraphBlock(wordBlockAsList));
       }

       return result;
   }

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

As explained in the Rendering Architecture, the Macro's execute method returns a list of Blocks. In the case of this simple macro it simply return a Word Block with the value if the parameter parameter (i.e. hello if the macro is called with {{simple-example parameter="hello"/}}).

And here's the ExampleMacroParameters.java file:

package com.acme;

import org.xwiki.properties.annotation.PropertyMandatory;
import org.xwiki.properties.annotation.PropertyDescription;

/**
 * Parameters for the {@link com.acme.internal.ExampleMacro} Macro.
 */

public class ExampleMacroParameters
{
   /**
     * @see {@link #getParameter()}
     */

   private String parameter;

   /**
     * @return the example parameter
     */

   public String getParameter()
   {
       return this.parameter;
   }
   
   /**
     * @param parameter the example parameter
     */

   @PropertyMandatory
   @PropertyDescription("Simple Example parameter")
   public void setParameter(String parameter)
   {
       this.parameter = parameter;
   }
}

Testing the Macro

The XWiki Rendering system has a pretty advanced Test framework to make it easy to test macros. Here is the test declaration for example1.test:

.runTransformations
.#-----------------------------------------------------
.input|xwiki/2.1
.# Test the macro in standalone mode
.#-----------------------------------------------------
{{simple-example parameter="hello"/}}
.#-----------------------------------------------------
.expect|xhtml/1.0
.#-----------------------------------------------------
<p>hello</p>
.#-----------------------------------------------------
.expect|event/1.0
.#-----------------------------------------------------
beginDocument
beginMacroMarkerStandalone [simple-example] [parameter=hello]
beginParagraph
onWord [hello]
endParagraph
endMacroMarkerStandalone [simple-example] [parameter=hello]
endDocument

This instructs the test framework to execute the macro with the given input and to compare to all specified outputs (defined using the expect keyword). In this example we're inputting XWiki Syntax 2.0 and comparing the result against XHTML 1.0 and against the internal events generated by the parser. These events are the pivot format used internally by the XWiki Rendering system. All the Renderers take those events to generate some output.

Note that the .runTransformations directives simply tells the test framework to execute the Macro Transformation on the XDOM generated by the input.

And here's the second test example2.test, this time testing the macro when used in inline mode:

.runTransformations
.#-----------------------------------------------------
.input|xwiki/2.1
.# Test the macro in inline mode
.#-----------------------------------------------------
This is inline {{simple-example parameter="hello"/}}
.#-----------------------------------------------------
.expect|xhtml/1.0
.#-----------------------------------------------------
<p>This is inline hello</p>
.#-----------------------------------------------------
.expect|event/1.0
.#-----------------------------------------------------
beginDocument
beginParagraph
onWord [This]
onSpace
onWord [is]
onSpace
onWord [inline]
onSpace
beginMacroMarkerInline [simple-example] [parameter=hello]
onWord [hello]
endMacroMarkerInline [simple-example] [parameter=hello]
endParagraph
endDocument

Finally this is how the IntegrationTests.java file looks like:

package com.acme;

import org.xwiki.rendering.test.integration.junit5.RenderingTests;

/**
 * Run all tests found in {@code *.test} files located in the classpath. These {@code *.test} files must follow the
 * conventions described in {@link org.xwiki.rendering.test.integration.TestDataParser}.
 */

public class IntegrationTests implements RenderingTests
{
}

Deploying the Macro

Now that we have a functioning Macro let's build it and deploy it.

A macro is an XWiki Component and thus is deployed like any component.

Once the macro has been deployed inside XWiki, you can use the following as an input to the XWiki Rendering Parser (in a XWiki instance you can put it in a page content for example):

{{simple-example parameter="hello"/}}

Enjoy!

Tips

Parsing Macro Content in Wiki Syntax

If your Macro content contains wiki syntax and you wish to parse it to generate XDOM blocks there's a very easy to do so. You just need to get injected a MacroContentParser as shown in this example:

...
import org.xwiki.rendering.macro.MacroContentParser;
...
public class MyMacro extends AbstractMacro<MyMacroParameters>
{
...
   @Inject
   private MacroContentParser contentParser;
...
   public List<Block> execute(MyMacroParameters parameters, String content, MacroTransformationContext context)
       throws MacroExecutionException
   {
       // Parse macro content here
       List<Block> blocks = this.contentParser.parse(content, context, true, context.isInline()).getChildren();

       return blocks;
   }
}

15.9+ 

It's also generally a good idea to prepare it so that it's not parsed again at each execution of the macro. MacroContentParser provides a helper for this (assuming your are using MacroContentParser):

...
import org.xwiki.rendering.macro.MacroContentParser;
...
public class MyMacro extends AbstractMacro<MyMacroParameters>
{
...
   @Inject
   private MacroContentParser contentParser;
...
   public List<Block> execute(MyMacroParameters parameters, String content, MacroTransformationContext context)
       throws MacroExecutionException
   {
       // Parse macro content here
       List<Block> blocks = this.contentParser.parse(content, context, true, context.isInline()).getChildren();

       return blocks;
   }
...
   public void prepare(MacroBlock macroBlock) throws MacroPreparationException
   {
       this.contentParser.prepareContentWiki(macroBlock);
   }
}

Finding the location of the Macro

You could be interested in finding the reference to the document where your Macro has been called. This can be achieved like this:

    @Inject
   private DocumentReferenceResolver<String> documentReferenceResolver;
...
   public List<Block> execute(ChartMacroParameters macroParams, String content, MacroTransformationContext context)
       throws MacroExecutionException
   {
        String source = extractSourceContentReference(context.getCurrentMacroBlock());
        DocumentReference reference = this.documentReferenceResolver.resolve(source);
       ...
   }
...
   private String extractSourceContentReference(Block source)
   {
        String contentSource = null;
        MetaDataBlock metaDataBlock =
            source.getFirstBlock(new MetadataBlockMatcher(MetaData.SOURCE), Block.Axes.ANCESTOR);
       if (metaDataBlock != null) {
            contentSource = (String) metaDataBlock.getMetaData().getMetaData(MetaData.SOURCE);
       }
       return contentSource;
   }

Inline editing content and parameter

Inline editing of content has been introduced in XWiki 10.10RC1 whereas inline editing of parameter has been introduced in XWiki 11.1RC1.

You might be interested in declaring some part of the macro content or some parameter as inline editable: the user will then be able to edit those parts of the content directly in the wysiwyg editor.

In order to make this available you need to specify 2 information when declaring the macro:

  1. the type of the macro content or of the parameter
  2. the parts of the macro that can be editable inline

Specify the type of the macro content

You need to specify it in the constructor of the DefaultContentDescriptor

public DefaultContentDescriptor(String description, boolean mandatory, Type type)

The type of a content which can be editable inline, is List<Block>.
In order to simplify its declaration, we created a constant that can be immediately used:

new DefaultContentDescriptor("Content of the message", true, Block.LIST_BLOCK_TYPE));

Specify the type of the macro parameter

You can use a dedicated annotation to use on the parameter:

@PropertyDisplayType({ List.class, Block.class })

Specify the parts of the macro that can be editable inline

When implementing the the Macro's execute method, you can specify which parts of the macro will be editable inline, by specifying some metadata.

For example, if you want to declare a block containing a logo which is always the same and a block which will be editable inline you can specify it like this:

ResourceReference imageReference = // declare the reference to the image logo
Block logoBlock = new ImageBlock(imageReference, true);
List<Block> content = this.contentParser.parse(content, context, false, context.isInline()).getChildren(); // parse the existing content and get its children blocks
Block editableContent = new MetadataBlock(content, getNonGeneratedContentMetadata()); // specifies the right metadata in order to make the content editable inline
return Arrays.asList(logoBlock, editableContent);

Please note that you can also specify which syntax will be used inside the macro content, by specifying a metadata containing the id of the syntax to use.

When dealing with macro parameters, you can use the same kind of utility method, except that you need to specify the name of the parameter. For example, if you're handling a parameter named title, you can write something like that:

new MetaDataBlock(titleBlock, getNonGeneratedContentMetaData("title"));

Note: the getNonGeneratedContentMetadata() method is a helper available in the AbstractMacro. It simply returns a MetaData object with a metadata named non-generated-content. You could do the same manually with: 

MetaData metaData = new MetaData();
metaData.addMetaData(MetaData.NON_GENERATED_CONTENT, "string value representing the type, e.g. java.util.List<org.xwiki.rendering.block.Block>");

Backlinks and links refactoring in macros

13.4.3+, 13.7+ Links are automatically extracted from content of macros which are using the Wiki macro content type to create backlinks, and those links are then automatically refactored when a page is moved or renamed. It's also possible for Java macros to provide a dedicated implementation of the MacroRefactoring component to allow extracting links for specific parameters for example, or to process differently the macro content to extract links.

Tags:
    
  • Powered by XWiki 14.10.18-node1. Hosted and managed by XWiki SAS

Get Connected