Applicable: | This tutorial applies to Confluence 5.9.1 and higher. |
Level of experience: | Advanced. |
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.
To complete this tutorial, you should:
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 2git 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.
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.
Open a Terminal and navigate to directory where you would like to keep your plugin code.
To create a plugin skeleton, run the following command:
1 2atlas-create-confluence-plugin
The atlas-
commands are part of the Atlassian Plugin SDK and automate some of the work of plugin development for you.
To identify your plugin, enter the following information.
group-id |
|
artifact-id |
|
version |
|
package |
|
Confirm your entries when prompted.
The SDK creates your project skeleton and puts it in a wikipedia-macro
directory.
Navigate to the wikipedia-macro
directory created in the previous step.
Delete the test directories.
1 2rm -rf ./src/test/java rm -rf ./src/test/resources/
Delete the unneeded Java class files.
1 2rm -rf ./src/main/java/com/example/plugins/tutorial/confluence/*
Import the project into your favorite IDE.
atlassian-plugin.xml
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.
Add Macro module right after <web-resource>
closing tag.
1 2<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>
Since we have declared icon attribute, download
and save icon under src/main/resources/images
.
Make sure that icon name is wikipedia-macro.png
.
Cut <resource type="download" name="images/" location="/images"/>
from web-resource
element and paste it inside
<atlassian-plugin>
tag.
This will make icon accessible with /download/resources/${atlassian.plugin.key}/images/wikipedia-macro.png
link.
To support internationalization, in wikipedia-macro.properties
, add the following:
1 2com.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.
Create a templates
directory under src/main/resources/
.
Create a new template wikipedia-macro.vm
and add the following lines:
1 2<div> <a class="wiki-search" href="$wiki.pageLink"><em>$placeholder</em></a> <span class="wiki-desc popup" style="display: none;">$wiki.shortDescription</span> </div>
We will use jQuery to show and hide tooltip with short description. Inside wikipedia-macro.js
, add the following script:
1 2define('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()
.
Add some styles in wikipedia-macro.css
.
1 2.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; }
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.
Here is a skeleton of macro.
1 2package 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.
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 2package 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.
1 2package 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.
Inject CacheManager
and WikipediaService
, and then retrieve cache from cacheManager
.
1 2private 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
).
Create a context map for Velocity and add the following:
WikipediaResponse
object retrieved from cache.map
– a context that comes from Macro Browser.1 2@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); }
At this point, you can run your plugin in Confluence. In this step, you do just that.
To start a local Confluence instance, run the following command:
1 2atlas-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[INFO] confluence started successfully in 132s at http://localhost:1990/confluence [INFO] Type Ctrl-D to shutdown gracefully [INFO] Type Ctrl-C to exit
Log in as admin
/ admin
.
The Confluence Dashboard appears.
To open editor, click Create button.
Open Macro Browser and find Wikipedia Macro.
Fill in required fields and refresh preview.
Rate this page: