Wiki source code of Writing a Macro

Last modified by Thomas Mortagne on 2023/10/20 17:46

Show last authors
1 {{box cssClass="floatinginfobox" title="**Contents**"}}
2 {{toc/}}
3 {{/box}}
4
5 This tutorial explains how to create an XWiki Rendering Macro in Java. See the [[Macro Transformation reference documentation>>extensions:Extension.RenderingMacroTransformation.WebHome]] for additional details.
6
7 Pre-requisites:
8
9 * You must be using JDK 1.8 or above.
10 * You must be using XWiki Rendering 3.0 or above.
11 * (optional) You need to have [[Maven installed>>http://maven.apache.org/]] if you want to use the Maven Archetype we're offering to easily create Macro projects using Maven.
12 * You should understand XWiki's Rendering [[Architecture>>Architecture]].
13
14 = General Principles =
15
16 Start by understanding the [[Macro execution process>>Architecture]].
17
18 In order to implement a new Macro you'll need to write 2 classes:
19
20 * 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.
21 * 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:(((
22 {{code language="java"}}
23 /**
24 * @return true if the macro can be inserted in some existing content such as a paragraph, a list item etc. For
25 * example if I have <code>== hello {{velocity}}world{{/velocity}}</code> then the Velocity macro must
26 * support the inline mode and not generate a paragraph.
27 */
28 boolean supportsInlineMode();
29
30 /**
31 * Executes the macro.
32 *
33 * @param parameters the macro parameters in the form of a bean defined by the {@link Macro} implementation
34 * @param content the content of the macro
35 * @param context the context of the macros transformation process
36 * @return the result of the macro execution as a list of Block elements
37 * @throws MacroExecutionException error when executing the macro
38 */
39 List<Block> execute(P parameters, String content, MacroTransformationContext context)
40 throws MacroExecutionException;
41 {{/code}}
42 )))
43
44 Then you'll need to register your Macro class with the [[Component Manager>>xwiki:Documentation.DevGuide.WritingComponents]] 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:
45
46 {{code language="none"}}
47 {{mymacro .../}}
48 {{/code}}
49
50 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##.
51
52 == Security considerations ==
53
54 The Macro Transformation Context can be set in "Restricted Mode". When set, this parameter indicates that rendering is performed in a context where:
55
56 * modifications to the database or other privileged operations should not be performed
57 * expensive computations should not be performed
58 * any potentially dangerous operations should not be performed (like executing scripts)
59
60 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.
61
62 == Macro preparation ==
63
64 {{version since="15.9"}}
65 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 importanr impact on performance for macros in which most of the work is not impacted by the runtime context.
66
67 For that you need to implement the following method:
68
69 {{code language="java"}}
70
71 /**
72 * Prepare a {@link MacroBlock} meant to be cached to be executed several times. The goal is to pre-execute
73 * everything that is independent of any context and store it in an annotation of the passed {@link MacroBlock}.
74 * <p>
75 * The result of the pre-execution is generally stored in the {@link MacroBlock} as attribute. Since the prepared
76 * block might end up exposed in an unsafe environment the value should be either clonable or immutable.
77 *
78 * @param macroBlock the macro block to prepare
79 */
80 default void prepare(MacroBlock macroBlock) throws MacroPreparationException
81 {{/code}}
82 {{/version}}
83
84 = Implementing a Macro =
85
86 Here are detailed steps explaining how you can create a macro and deploy it.
87
88 == Creating a Macro using Maven ==
89
90 In order for this job to go as smooth as possible we have created a [[Maven Archetype>>http://maven.apache.org/archetype/maven-archetype-plugin/]] to help you create a simple macro module with a single command.
91
92 After you've [[installed Maven>>http://maven.apache.org/]], open a shell prompt an type:
93 {{code language="none"}}mvn archetype:generate{{/code}}
94
95 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):
96
97 {{code language="none"}}
98 mvn archetype:generate \
99 -DarchetypeArtifactId=xwiki-rendering-archetype-macro \
100 -DarchetypeGroupId=org.xwiki.rendering \
101 -DarchetypeVersion=3.2
102 {{/code}}
103
104 Then follow the instructions. For example:
105
106 {{code language="none"}}
107 vmassol@tmp $ mvn archetype:generate
108 [INFO] Scanning for projects...
109 [INFO]
110 [INFO] ------------------------------------------------------------------------
111 [INFO] Building Maven Stub Project (No POM) 1
112 [INFO] ------------------------------------------------------------------------
113 [INFO]
114 [INFO] >>> maven-archetype-plugin:2.0:generate (default-cli) @ standalone-pom >>>
115 [INFO]
116 [INFO] <<< maven-archetype-plugin:2.0:generate (default-cli) @ standalone-pom <<<
117 [INFO]
118 [INFO] --- maven-archetype-plugin:2.0:generate (default-cli) @ standalone-pom ---
119 [INFO] Generating project in Interactive mode
120 [INFO] No archetype defined. Using maven-archetype-quickstart (org.apache.maven.archetypes:maven-archetype-quickstart:1.0)
121 Choose archetype:
122 ...
123 466: remote -> org.xwiki.rendering:xwiki-rendering-archetype-macro (Make it easy to create a maven project for creating XWiki Rendering Macros.)
124 ...
125 Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): 143: 466
126 Choose version:
127 1: 3.2-milestone-3
128 2: 3.2-rc-1
129 3: 3.2
130 Choose a number: 3:
131 Define value for property 'groupId': : com.acme
132 Define value for property 'artifactId': : example
133 Define value for property 'version': 1.0-SNAPSHOT: :
134 Define value for property 'package': com.acme: :
135 Confirm properties configuration:
136 groupId: com.acme
137 artifactId: example
138 version: 1.0-SNAPSHOT
139 package: com.acme
140 Y: : Y
141 [INFO] ------------------------------------------------------------------------
142 [INFO] BUILD SUCCESS
143 [INFO] ------------------------------------------------------------------------
144 [INFO] Total time: 12.248s
145 [INFO] Finished at: Fri Mar 11 14:54:46 CET 2011
146 [INFO] Final Memory: 7M/81M
147 [INFO] ------------------------------------------------------------------------
148 {{/code}}
149
150 Then go in the created directory (##example## in our example above) and run {{code}}mvn install{{/code}} to build your macro.
151
152 If you are a XWiki developer or you just want to see [[more examples of implemented macros>>Main.Macros]], you can check the {{scm project="xwiki-rendering" path="xwiki-rendering-macros"}}source code for the Macros in our SCM{{/scm}}.
153
154 Now, let's take a moment and examine the newly generated project. Navigating in the project's folder, we will see the following structure:
155
156 * ##pom.xml## - the project's POM file.
157 * ##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.
158 * ##src/main/java/.../internal/ExampleMacro.java## - the macro itself.
159 * ##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##).
160 * ##src/test/java/.../IntegrationTests.java## - JUnit Test Suite to run rendering tests for the Macro.
161 * ##src/test/resources/example1.test## - a test file for testing the Macro. It tests that the macro works when standalone.
162 * ##src/test/resources/example2.test## - a test file for testing the Macro. It tests that the macro works when inline.
163
164 == Macro Code ==
165
166 Here's the content of our generated ##ExampleMacro.java##.
167
168 {{code language="java"}}
169 /*
170 * See the NOTICE file distributed with this work for additional
171 * information regarding copyright ownership.
172 *
173 * This is free software; you can redistribute it and/or modify it
174 * under the terms of the GNU Lesser General Public License as
175 * published by the Free Software Foundation; either version 2.1 of
176 * the License, or (at your option) any later version.
177 *
178 * This software is distributed in the hope that it will be useful,
179 * but WITHOUT ANY WARRANTY; without even the implied warranty of
180 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
181 * Lesser General Public License for more details.
182 *
183 * You should have received a copy of the GNU Lesser General Public
184 * License along with this software; if not, write to the Free
185 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
186 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
187 */
188 package com.acme.internal;
189
190 import java.util.Collections;
191 import java.util.List;
192
193 import javax.inject.Named;
194
195 import org.xwiki.component.annotation.Component;
196 import org.xwiki.rendering.block.Block;
197 import org.xwiki.rendering.block.ParagraphBlock;
198 import org.xwiki.rendering.block.WordBlock;
199 import org.xwiki.rendering.macro.AbstractMacro;
200 import org.xwiki.rendering.transformation.MacroTransformationContext;
201
202 import static java.util.Collections.singleton;
203
204 /**
205 * Example Macro.
206 *
207 * @version $Id$
208 * @since X.Y.X
209 */
210 @Component
211 @Named("example")
212 public class ExampleMacro extends AbstractMacro<ExampleMacroParameters>
213 {
214 /**
215 * The description of the macro.
216 */
217 private static final String DESCRIPTION = "Example Macro";
218
219 /**
220 * Create and initialize the descriptor of the macro.
221 */
222 public ExampleMacro()
223 {
224
225 super("Example", DESCRIPTION, ExampleMacroParameters.class);
226 setDefaultCategories(singleton("Tutorial"));
227 }
228
229 @Override
230 public List<Block> execute(ExampleMacroParameters parameters, String content, MacroTransformationContext context)
231 {
232 List<Block> result;
233
234 List<Block> wordBlockAsList = Collections.singletonList(new WordBlock(parameters.getParameter()));
235
236 // Handle both inline mode and standalone mode.
237 if (context.isInline()) {
238 result = wordBlockAsList;
239 } else {
240 // Wrap the result in a Paragraph Block since a WordBlock is an inline element and it needs to be
241 // inside a standalone block.
242 result = Collections.singletonList(new ParagraphBlock(wordBlockAsList));
243 }
244
245 return result;
246 }
247
248 @Override
249 public boolean supportsInlineMode()
250 {
251 return true;
252 }
253 }
254
255 {{/code}}
256
257 As explained in the [[Rendering Architecture>>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 ##~{~{example parameter="hello"/}}##).
258
259 And here's the ##ExampleMacroParameters.java## file:
260
261 {{code language="java"}}
262 package com.acme;
263
264 import org.xwiki.properties.annotation.PropertyMandatory;
265 import org.xwiki.properties.annotation.PropertyDescription;
266
267 /**
268 * Parameters for the {@link com.acme.internal.ExampleMacro} Macro.
269 */
270 public class ExampleMacroParameters
271 {
272 /**
273 * @see {@link #getParameter()}
274 */
275 private String parameter;
276
277 /**
278 * @return the example parameter
279 */
280 public String getParameter()
281 {
282 return this.parameter;
283 }
284
285 /**
286 * @param parameter the example parameter
287 */
288 @PropertyMandatory
289 @PropertyDescription("Example parameter")
290 public void setParameter(String parameter)
291 {
292 this.parameter = parameter;
293 }
294 }
295 {{/code}}
296
297 == Testing the Macro ==
298
299 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##:
300
301 {{code language="none"}}
302 .runTransformations
303 .#-----------------------------------------------------
304 .input|xwiki/2.0
305 .# Test the macro in standalone mode
306 .#-----------------------------------------------------
307 {{example parameter="hello"/}}
308 .#-----------------------------------------------------
309 .expect|xhtml/1.0
310 .#-----------------------------------------------------
311 <p>hello</p>
312 .#-----------------------------------------------------
313 .expect|event/1.0
314 .#-----------------------------------------------------
315 beginDocument
316 beginMacroMarkerStandalone [example] [parameter=hello]
317 beginParagraph
318 onWord [hello]
319 endParagraph
320 endMacroMarkerStandalone [example] [parameter=hello]
321 endDocument
322 {{/code}}
323
324 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.
325
326 Note that the ##.runTransformations## directives simply tells the test framework to execute the Macro Transformation on the XDOM generated by the input.
327
328 And here's the second test ##example2.test##, this time testing the macro when used in inline mode:
329
330 {{code language="none"}}
331 .runTransformations
332 .#-----------------------------------------------------
333 .input|xwiki/2.0
334 .# Test the macro in inline mode
335 .#-----------------------------------------------------
336 This is inline {{example parameter="hello"/}}
337 .#-----------------------------------------------------
338 .expect|xhtml/1.0
339 .#-----------------------------------------------------
340 <p>This is inline hello</p>
341 .#-----------------------------------------------------
342 .expect|event/1.0
343 .#-----------------------------------------------------
344 beginDocument
345 beginParagraph
346 onWord [This]
347 onSpace
348 onWord [is]
349 onSpace
350 onWord [inline]
351 onSpace
352 beginMacroMarkerInline [example] [parameter=hello]
353 onWord [hello]
354 endMacroMarkerInline [example] [parameter=hello]
355 endParagraph
356 endDocument
357 {{/code}}
358
359 Finally this is how the ##IntegrationTests.java## file looks like:
360
361 {{code language="java"}}
362 package com.acme;
363
364 import org.junit.runner.RunWith;
365 import org.xwiki.rendering.test.integration.RenderingTestSuite;
366
367 /**
368 * Run all tests found in {@code *.test} files located in the classpath. These {@code *.test} files must follow the
369 * conventions described in {@link org.xwiki.rendering.test.integration.TestDataParser}.
370 */
371 @RunWith(RenderingTestSuite.class)
372 public class IntegrationTests
373 {
374 }
375 {{/code}}
376
377 = Deploying the Macro =
378
379 Now that we have a functioning Macro let's build it and deploy it.
380
381 A macro is an [[XWiki Component>>extensions:Extension.Component Module]] and thus is [[deployed like any component>>xwiki:Documentation.DevGuide.Tutorials.WritingComponents.WebHome#HDeployingtheComponent]].
382
383 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):
384
385 {{code language="none"}}
386 {{example parameter="hello"/}}
387 {{/code}}
388
389 Enjoy!
390
391 = Tips =
392
393 == Parsing Macro Content in Wiki Syntax ==
394
395 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:
396
397 {{code language="java"}}
398 ...
399 import org.xwiki.rendering.macro.MacroContentParser;
400 ...
401 public class MyMacro extends AbstractMacro<MyMacroParameters>
402 {
403 ...
404 @Inject
405 private MacroContentParser contentParser;
406 ...
407 public List<Block> execute(MyMacroParameters parameters, String content, MacroTransformationContext context)
408 throws MacroExecutionException
409 {
410 // Parse macro content here
411 List<Block> blocks = this.contentParser.parse(content, context, true, context.isInline()).getChildren();
412
413 return blocks;
414 }
415 }
416 {{/code}}
417
418 {{version since="15.9"}}
419 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##):
420
421 {{code language="java"}}
422 ...
423 import org.xwiki.rendering.macro.MacroContentParser;
424 ...
425 public class MyMacro extends AbstractMacro<MyMacroParameters>
426 {
427 ...
428 @Inject
429 private MacroContentParser contentParser;
430 ...
431 public List<Block> execute(MyMacroParameters parameters, String content, MacroTransformationContext context)
432 throws MacroExecutionException
433 {
434 // Parse macro content here
435 List<Block> blocks = this.contentParser.parse(content, context, true, context.isInline()).getChildren();
436
437 return blocks;
438 }
439 ...
440 public void prepare(MacroBlock macroBlock) throws MacroPreparationException
441 {
442 this.contentParser.prepareContentWiki(macroBlock);
443 }
444 }
445 {{/code}}
446 {{/version}}
447
448 == Finding the location of the Macro ==
449
450 You could be interested in finding the reference to the document where your Macro has been called. This can be achieved like this:
451
452 {{code language="java"}}
453 @Inject
454 private DocumentReferenceResolver<String> documentReferenceResolver;
455 ...
456 public List<Block> execute(ChartMacroParameters macroParams, String content, MacroTransformationContext context)
457 throws MacroExecutionException
458 {
459 String source = extractSourceContentReference(context.getCurrentMacroBlock());
460 DocumentReference reference = this.documentReferenceResolver.resolve(source);
461 ...
462 }
463 ...
464 private String extractSourceContentReference(Block source)
465 {
466 String contentSource = null;
467 MetaDataBlock metaDataBlock =
468 source.getFirstBlock(new MetadataBlockMatcher(MetaData.SOURCE), Block.Axes.ANCESTOR);
469 if (metaDataBlock != null) {
470 contentSource = (String) metaDataBlock.getMetaData().getMetaData(MetaData.SOURCE);
471 }
472 return contentSource;
473 }
474 {{/code}}
475
476 == Inline editing content and parameter ==
477
478 {{warning}}
479 Inline editing of content has been introduced in XWiki 10.10RC1 whereas inline editing of parameter has been introduced in XWiki 11.1RC1.
480 {{/warning}}
481
482 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>>extensions:Extension.CKEditor Integration.WebHome]].
483
484 In order to make this available you need to specify 2 information when declaring the macro:
485
486 1. the type of the macro content or of the parameter
487 1. the parts of the macro that can be editable inline
488
489 === Specify the type of the macro content ===
490
491 You need to specify it in the constructor of the {{code}}DefaultContentDescriptor{{/code}}:
492
493 {{code language="java"}}
494 public DefaultContentDescriptor(String description, boolean mandatory, Type type)
495 {{/code}}
496
497 The type of a content which can be editable inline, is {{code}}List<Block>{{/code}}.
498 In order to simplify its declaration, we created a constant that can be immediately used:
499
500 {{code language="java"}}
501 new DefaultContentDescriptor("Content of the message", true, Block.LIST_BLOCK_TYPE));
502 {{/code}}
503
504 === Specify the type of the macro parameter ===
505
506 You can use a dedicated annotation to use on the parameter:
507
508 {{code language="java"}}
509 @PropertyDisplayType({ List.class, Block.class })
510 {{/code}}
511
512 === Specify the parts of the macro that can be editable inline ===
513
514 When implementing the the Macro's {{code}}execute{{/code}} method, you can specify which parts of the macro will be editable inline, by specifying some metadata.
515
516 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:
517
518 {{code language="java"}}
519 ResourceReference imageReference = // declare the reference to the image logo
520 Block logoBlock = new ImageBlock(imageReference, true);
521 List<Block> content = this.contentParser.parse(content, context, false, context.isInline()).getChildren(); // parse the existing content and get its children blocks
522 Block editableContent = new MetadataBlock(content, getNonGeneratedContentMetadata()); // specifies the right metadata in order to make the content editable inline
523 return Arrays.asList(logoBlock, editableContent);
524 {{/code}}
525
526 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.
527
528 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:
529
530 {{code language="java"}}
531 new MetaDataBlock(titleBlock, getNonGeneratedContentMetaData("title"));
532 {{/code}}
533
534 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:
535
536 {{code language="java"}}
537 MetaData metaData = new MetaData();
538 metaData.addMetaData(MetaData.NON_GENERATED_CONTENT, "string value representing the type, e.g. java.util.List<org.xwiki.rendering.block.Block>");
539 {{/code}}
540
541 == Backlinks and links refactoring in macros ==
542
543 {{version since="13.4.3,13.7RC1"}}
544 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>>https://github.com/xwiki/xwiki-platform/blob/master/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/main/java/org/xwiki/rendering/macro/MacroRefactoring.java]] to allow extracting links for specific parameters for example, or to process differently the macro content to extract links.
545 {{/version}}
  • Powered by XWiki 14.10.18-node1. Hosted and managed by XWiki SAS

Get Connected