Last updatedApr 25, 2018

Creating a Wikipedia macro

Applicable:

This tutorial applies to Confluence Server 5.9.1 and higher.

Level of experience:

Advanced.

Overview of the tutorial

In this tutorial, you will learn how to create a macro that fetches resources from external API and renders them inside Confluence page. For this tutorial, we will use Wikipedia API. The macro will search for some phrase, fetch first page from search result, and show its short description in a tooltip on hover. We will cache response using Atlassian Cache.

Before you begin

To complete this tutorial, you should:

  1. Understand the basics of Java development: classes, interfaces, methods, how to use the compiler, and so on.
  2. Work through the Atlassian plugin SDK tutorial.
  3. Have a basic knowledge of JavaScript.

Plugin source

If you want to skip ahead or check your work when you finish, you can find the plugin source code on Atlassian Bitbucket. Alternatively, you can  download the source as a ZIP archive

To clone the repository, run the following command:

1
git clone git@bitbucket.org:atlassian_tutorial/wikipedia-macro.git

About these instructions

You can use any supported combination of operating system and IDE to create this plugin. These instructions were written using Intellij IDEA 2017.3 on macOS Sierra. If you are using another operating system or IDE combination, you should use the equivalent operations for your specific environment. This tutorial was last tested with Confluence 6.7.1 using the Atlassian SDK 6.3.10.

Step 1. Create the plugin project and prune the skeleton

In this step, you'll generate skeleton code for your plugin. Because you won't need many of the skeleton files, you also delete them in this step. 

  1. Open a Terminal and navigate to directory where you would like to keep your plugin code.
  2. To create a plugin skeleton, run the following command:

    1
    atlas-create-confluence-plugin

    The atlas- commands are part of the Atlassian Plugin SDK and automate some of the work of plugin development for you.  

  3. To identify your plugin, enter the following information.

    group-id

    com.example.plugins.tutorial.confluence

    artifact-id

    wikipedia-macro

    version

    1.0-SNAPSHOT

    package

    com.example.plugins.tutorial.confluence

  4. Confirm your entries when prompted.
    The SDK creates your project skeleton and puts it in a wikipedia-macro directory. 

  5. Navigate to the wikipedia-macro directory created in the previous step.

  6. Delete the test directories.

    1
    2
    rm -rf ./src/test/java
    rm -rf ./src/test/resources/
  7. Delete the unneeded Java class files.

    1
    rm -rf ./src/main/java/com/example/plugins/tutorial/confluence/*
  8. Import the project into your favorite IDE.

Step 2. Define the macro in atlassian-plugin.xml

  1. To specify context for web resources, inside web-resources, replace the auto generated context with the following:

    1
    2
    <context>viewcontent</context>
    <context>preview</context>

    This will include resources in page view, page preview, and macro preview.

  2. Add Macro module right after <web-resource> closing tag.

    1
    2
    3
    4
    5
    6
    7
    8
    <xhtml-macro name="wikipedia-macro" key="wikipedia-macro" class="com.example.plugins.tutorial.confluence.WikipediaMacro"
                 icon="/download/resources/${atlassian.plugin.key}/images/wikipedia-macro.png">
        <category name="external-content"/>
        <parameters>
            <parameter name="search" type="string" required="true"/>
            <parameter name="placeholder" type="string" required="true"/>
        </parameters>
    </xhtml-macro>
  3. Since we have declared icon attribute, download and save icon under src/main/resources/images. Make sure that icon name is wikipedia-macro.png.

  4. Cut <resource type="download" name="images/" location="/images"/> from web-resource element and paste it inside <atlassian-plugin> tag.

    1
    This will make icon accessible with `/download/resources/${atlassian.plugin.key}/images/wikipedia-macro.png` link.
  5. To support internationalization, in wikipedia-macro.properties, add the following:

1
2
com.example.plugins.tutorial.confluence.wikipedia-macro.wikipedia-macro.label=Wikipedia Macro
com.example.plugins.tutorial.confluence.wikipedia-macro.wikipedia-macro.desc=Shows short information about Wikipedia page on hover

For more information, check i18n conventions for macro.

Step 3. Create resource files

  1. Create a templates directory under src/main/resources/.
  2. Create a new template wikipedia-macro.vm and add the following lines:

    1
    2
    3
    4
    <div>
        <a class="wiki-search" href="$wiki.pageLink"><em>$placeholder</em></a>
        <span class="wiki-desc popup" style="display: none;">$wiki.shortDescription</span>
    </div>
  3. We will use jQuery to show and hide tooltip with short description. Inside wikipedia-macro.js, add the following script:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    define('wikipedia-macro',['ajs'], function(AJS){
        AJS.toInit(function () {
            console.log('Loading wiki macro');
            AJS.$('.wiki-search').hover(function(){
                AJS.$(this).parent().find('.wiki-desc').fadeIn(400);
            }, function () {
                AJS.$(this).parent().find('.wiki-desc').fadeOut(400);
            });
        });
    });
    
    require('wikipedia-macro');

    AJS.toInit() method is a wrapper of $(document).ready().

  4. Add some styles in wikipedia-macro.css.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    .popup {
        background: #fff;
        box-shadow: 0 0 10px rgba(0,0,0,1);
        width: 300px;
        position: fixed;
        z-index: 999;
        padding: 10px;
        border-radius: 15px;
        font-size: 10px;
    }

Step 4. Create Java classes

  1. Implement com.atlassian.confluence.macro.Macro interface. We already defined a com.example.plugins.tutorial.confluence.WikipediaMacro as a value of class attribute in xhtml-macro module.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    Here is a skeleton of macro.
    
    ``` java
    package com.example.plugins.tutorial.confluence;
    
    import com.atlassian.cache.Cache;
    import com.atlassian.cache.CacheManager;
    import com.atlassian.confluence.content.render.xhtml.ConversionContext;
    import com.atlassian.confluence.macro.Macro;
    import com.atlassian.confluence.macro.MacroExecutionException;
    import com.atlassian.confluence.util.velocity.VelocityUtils;
    import com.atlassian.plugin.spring.scanner.annotation.component.Scanned;
    import com.atlassian.plugin.spring.scanner.annotation.imports.ConfluenceImport;
    import com.google.common.collect.ImmutableMap;
    
    import javax.inject.Inject;
    import java.util.Map;
    import java.util.Optional;
    
    @Scanned
    public class WikipediaMacro implements Macro {
    
        @Override
        public String execute(Map<String, String> map, String s, ConversionContext conversionContext) throws MacroExecutionException {
            return null;
        }
    
        @Override
        public BodyType getBodyType() {
            return BodyType.NONE;
        }
    
        @Override
        public OutputType getOutputType() {
            return OutputType.INLINE;
        }
    }
    ```
    
    Our macro doesn't have body and can be rendered inline.
  2. Now we need a service that will fetch pages. We will simply pass search parameter to the query and pick the first available page. We extract a snippet and pageid from response, and then save it to WikipediaResponse object that will be passed to Velocity later.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    ``` java
    package com.example.plugins.tutorial.confluence;
    
    import com.atlassian.confluence.util.http.HttpResponse;
    import com.atlassian.confluence.util.http.HttpRetrievalService;
    import com.atlassian.plugin.spring.scanner.annotation.imports.ConfluenceImport;
    import com.google.gson.JsonElement;
    import com.google.gson.JsonObject;
    import com.google.gson.JsonParser;
    import org.apache.commons.httpclient.util.URIUtil;
    import org.apache.commons.io.IOUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import javax.annotation.Nonnull;
    import javax.inject.Inject;
    import javax.inject.Named;
    import java.io.IOException;
    import java.util.Objects;
    import java.util.Optional;
    
    @Named
    public class WikipediaService {
    
        @ConfluenceImport
        private HttpRetrievalService service;
        private static final Logger log = LoggerFactory.getLogger(WikipediaService.class);
        private static final String WIKI_URL_TEMPLATE =
                "https://en.wikipedia.org/w/api.php?format=json&action=query&generator=search&gsrsearch=%s&gsrlimit=1&" +
                        "prop=extracts&exintro&explaintext&exsentences=2";
    
        @Inject
        public WikipediaService(HttpRetrievalService service) {
            this.service = service;
        }
    
        @Nonnull
        public Optional<WikipediaResponse> searchByText(String searchText) {
            try {
                HttpResponse response = service.get((String.format(WIKI_URL_TEMPLATE, URIUtil.encodeAll(searchText))));
                JsonParser parser = new JsonParser();
                JsonElement body = parser.parse(IOUtils.toString(response.getResponse(), "UTF-8"));
                JsonElement result = getFirstPage(body);
                String snippet = result.getAsJsonObject().getAsJsonPrimitive("extract").getAsString();
                Integer pageId = result.getAsJsonObject().getAsJsonPrimitive("pageid").getAsInt();
                return Optional.of(new WikipediaResponse(String.format("https://en.wikipedia.org/?curid=%d", pageId), snippet));
            } catch (IOException e){
                log.error("Exception during request to Wikipedia", e);
                return Optional.empty();
            }
        }
    
        private JsonElement getFirstPage(JsonElement body){
            JsonObject query = Objects.requireNonNull(body.getAsJsonObject().getAsJsonObject("query"), "Response body doesn't contain \"query\" key");
            return query.getAsJsonObject("pages").entrySet().stream().findFirst().get().getValue();
        }
    }
    ```
    
    Here is `WikipediaResponse` class.
    
    ``` java
    package com.example.plugins.tutorial.confluence;
    
    public class WikipediaResponse {
        private String pageLink;
        private String shortDescription;
    
        public WikipediaResponse(String pageLink, String shortDescription) {
            this.pageLink = pageLink;
            this.shortDescription = shortDescription;
        }
    
        public String getPageLink() {
            return pageLink;
        }
    
        public void setPageLink(String pageLink) {
            this.pageLink = pageLink;
        }
    
        public String getShortDescription() {
            return shortDescription;
        }
    
        public void setShortDescription(String shortDescription) {
            this.shortDescription = shortDescription;
        }
    }
    ```
    
    So, we have a `pageLink` field that is used to insert a link to Wikipedia page and `shortDescription` field to store
    short description of this page.  
    
    Let's put some logic in `WikipediaMacro` class.
  3. Inject CacheManager and WikipediaService, and then retrieve cache from cacheManager.

    1
    2
    3
    4
    5
    6
    private Cache<String, Optional<WikipediaResponse>> cache;
    
    @Inject
    public WikipediaMacro(WikipediaService service, @ConfluenceImport CacheManager cacheManager) {
        cache = cacheManager.getCache("wikipedia-macro", service::searchByText);
    }

    The second argument of getCache method is CacheLoader. Keep in mind that its load method should return nonnull value. If null values are possible, declare the loader's value type to be a wrapper type (for example, Optional).

  4. Create a context map for Velocity and add the following:

    1. WikipediaResponse object retrieved from cache.
    2. map – a context that comes from Macro Browser.
    1
    2
    3
    4
    5
    6
    @Override
    public String execute(Map<String, String> map, String s, ConversionContext conversionContext) throws MacroExecutionException {
        Map context = ImmutableMap.builder().put("wiki", cache.get(map.get("search")).orElseThrow(
                () -> new MacroExecutionException("Unable to retrieve response from Wikipedia"))).putAll(map).build();
        return VelocityUtils.getRenderedTemplate("templates/wikipedia-macro.vm", context);
    }

Step 5. Test your work

At this point, you can run your plugin in Confluence. In this step, you do just that.  

  1. To start a local Confluence instance, run the following command:

    1
    atlas-run

    This command takes a minute or so to run. It builds your plugin code, starts a Confluence instance, and installs your plugin. When the process is finished, you see many status lines on your screen concluding with something like the following:

    1
    2
    3
    [INFO] confluence started successfully in 132s at http://localhost:1990/confluence
    [INFO] Type Ctrl-D to shutdown gracefully
    [INFO] Type Ctrl-C to exit
  2. Log in as admin / admin.
    The Confluence Dashboard appears.

  3. To open editor, click Create button.
  4. Open Macro Browser and find Wikipedia Macro.
  5. Fill in required fields and refresh preview.

Wikipedia macro preview