Hi all, I’m starting a new, four part blog series today on plugin architecture. Like all good series, I’m going to start with Episode IV, and it’ll probably jump the shark when it finally hits Episode I. We’ll see, I guess.

In this series, we’re going to create a search plugin that works across all Atlassian products. In the process, we’re going to hit a lot of different areas of the platform, including various plugin module types, the Shared Access Layer (SAL) and the Atlassian User Interface (AUI) library; a bit of the underlying frameworks on which our platform is based, such as Spring; as well as many of the best practices we’ve collectively learned over the last couple years. The many technologies and techniques we use in developing plugins at Atlassian aren’t really tricky at all to understand individually, and should be very accessible to relatively novice developers, but the way we combine all of them isn’t necessarily as obvious or intuitive, so that’s what we’ll be paying closest attention to in this series.

If you want to follow along, you will need the Atlassian Plugin SDK and a JDK of some sort. An IDE would not be a bad idea either; I’ll be using IDEA.

Step 1: A Vanilla Servlet

Okay, let’s begin. As ambitious as we’d like to be with our fancy cross-product search plugin, we have to start somewhere, and I find it’s always easiest to start with something very concrete and refactor towards the abstract later. So we’re going to start, just for the sake of concreteness, with a simple Confluence servlet plugin. I figure that Confluence comes with some searchable default content out of the box, so that will make testing easier. I’ll just be careful not to tie myself to any Confluence-specific APIs in the process, so that eventually moving cross-product will be as painless as possible.

The Plugin SDK provides a Maven Archetype — a plugin template — for each product, and a shell or batch script to simplify generating a plugin from its archetype. In our case, we’ll use atlas-create-confluence-plugin and see what it produces:

1 - no macro.png
The first place I like to look, when I’m trying to understand what a plugin does based on its code, is at its plugin descriptor. The plugin descriptor is a file called atlassian-plugin.xml in the src/main/resources directory of your plugin source; roughly speaking, it identifies your plugin and all of its modules to the plugin framework running within an Atlassian product. Here, the Confluence plugin archetype is evidently set up for creating a new macro…

Right, so that would be really helpful if we were actually writing a Confluence macro. However, we’re just writing a servlet, and decided to use Confluence as our initial testbed. So let’s delete the stuff we don’t want, and while we’re at it, clean up some other bits that are superfluous at the moment:

2 - test crapware.png
The Maven POM — roughly analogous to a makefile or an Ant buildfile — for this plugin shows a number of dependencies for automated testing that we don’t really need yet. I always prefer to minimize dependencies, so it’s time for some sweet delete-key action

3 - clean.png
Ahhh, much better. The only dependencies we have now are on Confluence itself, which we’d expect more or less, and on the maven-confluence-plugin which essentially provides the guts of the SDK. Now we can get started on our servlet.
As before, the plugin descriptor is the place to start. We need to add a servlet plugin module to the descriptor. The pattern that many of the plugin module types follow is to provide an interface or abstract class which the plugin developer implements or extends. In this case, we just need to extend HttpServlet, and our implementation can be very simple to start with:

4 - simple servlet source.png
Not much to it, I just added a servlet module entry to the plugin descriptor, pointed it at my implementation of HttpServlet, gave it a URL, and filled in doGet(). We actually have something runnable now, so in the spirit of instant gratification, let’s run it.
Open up a shell or command prompt, type atlas-run. This is another script provided by the SDK, which internally invokes mvn confluence:run. It automatically pulls down all dependencies it needs and, after a minute or two, you have a Confluence instance running on your local machine, using a time-bombed developer license, with the plugin loaded and enabled.

The servlet plugin module doc tells us we can find our servlet running under plugins/servlet in the application’s context path, and the Plugin SDK tells us that the context path is http://localhost:1990/confluence, so let’s see what’s there:

5 - simple servlet rendered.png
Rock and roll. Well, not very rock and roll… it doesn’t look very Atlassian-ish or Confluence-ish, but let’s say it’s a good start and have a chocolate.

Step 2: Less Ugly Please

Chocolate consumed, back to work. For our first “milestone” (where I’ll call this blog post good enough and go home) we’ll get our servlet looking more integrated with Confluence, and the code cleaned up a bit. Our search plugin still won’t be able to actually search anything, but hey, I need to leave some material for “Episode V.”

First off, we don’t want to be generating HTML markup directly from within Java source, the way we have so far. It’s important to keep user interface code cleanly separated from application logic, even though we don’t really have any logic yet, and it’s ugly besides. Okay, this is a problem that’s been solved over and over for more than 30 years… Let’s not reinvent the wheel; instead, let’s use the Atlassian Template Renderer, which is a component provided by the plugin development platform.

The template renderer uses Apache Velocity by default, so we’ll be using VTL as our templating language, but that’s not important right now. For the moment, I just want to focus on how we get an instance of the template renderer.

First, as usual, we need something in our plugin descriptor. To declare that we want to use a component from the plugin system (whether it’s provided by the host application, by the plugin system itself, or by another plugin), we need a component import module. The good news is, this module type is even more straightforward than the servlet module we used: specify the interface you’re going to use, and that’s it. The bad news is that the full, package-qualified name of the interface is ridiculous: com.atlassian.templaterenderer.velocity.one.six.VelocityTemplateRenderer. Sorry about that.

Moving on, we also need to provide the template renderer API to the compiler, since unlike HttpServlet it’s not part of the JDK. In the <dependencies> section of the Maven POM, where we blew away the testing dependencies we didn’t need, we’ll introduce a new dependency with provided scope, indicating that the host application will be able to provide instances of the template renderer at runtime (that is, we don’t need to package it into our plugin JAR).

Finally, we need to actually use the template renderer in our servlet. I’ll create a constructor for the servlet since we didn’t have one yet, make it take a TemplateRenderer, and then use it in doGet() to render my Velocity template out to the HTTP response:

6 - fancy servlet source.png
But, hang on… Who’s instantiating SearchServlet at runtime, and how is it pulling this TemplateRenderer out of thin air? We didn’t have to really think about that particular bit of magic before, but you might be wondering about it now.

Behind the scenes, the plugin system is doing dependency injection for you. More specifically, it’s using Spring to do constructor injection: constructing the SearchServlet instance with a TemplateRenderer implementation that’s been provided to the plugin system through some other mysterious mechanism. Since I want to keep this blog moving, I’ll just say this second mechanism is “magic” for now and come back to it later. All you need to know for the moment is that declaring the <component-import> is what makes some TemplateRenderer instances available for injection into your code.

Now, aside from injecting the renderer, the other thing I need to highlight is the <meta> tag in the HTML code. That wasn’t in there before, when we had the HTML hardcoded in a Java string. That “decorator” is used by SiteMesh, which floats around in the plugin system somewhere as a servlet filter, and whose job it is to provide consistent “chrome” for all pages served by the host application. Our servlet writes some simple HTML output to the HTTP response body, and the SiteMesh filter transforms and wraps that content as requested in the <meta> tag. In our example, we used atl.general as the decorator.

Note that we could have done this decoration without having the template renderer in play. That is, we could have just inserted the same <meta> tag into our hardcoded Java string, and that would have worked fine assuming we specified a content type of text/html for the response. If you’re working on a plugin for which you want to use a different templating system entirely (for example, you’re writing your plugin in Scala and Lift templates), that’s not a problem.

Okay, let’s see what we’ve got now:

7 - fancy servlet rendered.png
Right on. For really real this time. We have a servlet that we would be proud to use as the basis for a real search plugin. The one remaining issue (aside from obviously not having actually implemented search) is that unless you know the URL for the servlet, you won’t be able to find it; it’s not linked to from anywhere yet.

Step 3: Tying Off Loose Ends

The last I thing want to do before calling it a day is to get this servlet page linked from someplace convenient in the main Confluence UI. The best and easiest way to do this is using the cryptically named “Web Item” plugin module type. A web item, in a nutshell, is a hyperlink that’s inserted by your plugin into some standard place in the host application, such as the administration menu. In our case, I think the most convenient place to link to our search servlet from will be the “Browse” menu that’s on the top of all Confluence pages.

As usual, the first step in adding new functionality to the plugin is to declare the new plugin module in the plugin descriptor. Looking at the documentation again, we can see that aside from the module’s key attribute (required for all plugin modules) there are three interesting bits of information we need to provide:

  • The section into which the link will be inserted,
  • The <label> for the link (i.e. the link text),
  • The <link> itself.

For the Confluence “Browse” menu, the section we need is listed in the Confluence developer documentation on web UI locations. One thing worth pointing out, just as an aside: it’s sometimes tricky navigating our developer docs, especially when most of what you want to do is product-agnostic (and consequently documented in the PLUGINFRAMEWORK space), but you need some product-specific details, such as Confluence’s web item locations (documented in the CONFDEV space). Knowing where to hunt for information across these different spaces ends up being pretty important as you’re digging into plugin development.

Okay, now that we’ve found the identifier for the section we need, the last remaining wrinkle is how to fill out the <label> key. The documentation says we need an internationalization key, but we haven’t given any thought to i18n yet. Well, now’s as good a time as any to introduce this; all we need is one extra plugin module type, a “Web Resource,” to point at the default localizations we’ll use. Web resources are more generally applicable than just to i18n — they’re most commonly used for including groups of images, stylesheets and scripts in a plugin — but for now we’ll just stick to i18n. Here’s how our code ends up looking with both the web item and the i18n resource implemented:

8 - i18n source.png
Not bad at all, really: one extra property file containing our localizations, and the two new plugin modules. The one additional change I made, that you might have noticed, is to finally ditch our “Hoho” placeholder text and cash in on a bit of the benefits that we can get from the Atlassian Template Renderer. The default “renderer context” is pre-populated with a few handy classes, including an I18nResolver intuitively named i18n. So in one line of VTL, I can grab the localized “Search” text that I’m using in the web item label, and use it for the search servlet’s page title.

Enough coding, how does this all look at the end of the day?

9 - i18n web-item rendered.png
Sweet, the web item shows up in the “Browse” menu…

10 - i18n servlet rendered.png
… including the “Browse” menu in our servlet page. Miller time.

Summary

Okay, so we now have a cross-product search servlet that is neither cross-product nor capable of searching whatsoever. This is perhaps somewhat anticlimactic. However we’ve laid some really important groundwork in this post, both in the technical foundation for the rest of the plugin, that we’ll continue building in subsequent blog posts here, and hopefully in your knowledge of plugin development fundamentals as well.

I really want to emphasize that we’ve covered a huge amount of ground here already. As I said before, I don’t think that any of the individual bits and pieces I’ve shown you here are too fancy or mindbending at all, but there were quite a lot of these bits and pieces that I ended up using together. If you’ve been able to follow most of how everything combined here, you’re in good shape for “Episode V.” To recap, we looked at:

  • Maven basics: the POM, the Confluence plugin archetype and the Plugin SDK.
  • The plugin descriptor: finding appropriate plugin module types in the developer docs and adding them to the plugin descriptor.
  • Some specific plugin module types: servlet, component-import, web-item and web-resource.
  • The template renderer: instantiating, basic usage and the I18nResolver that’s automatically provided in the renderer context.
  • Dependency injectionusing the wonderful, magical component-import (which we’ll see much more of next time).
  • Integrated look-and-feel using SiteMesh and web-items.

Please feel free to download a snapshot of the code that we have written so far, test it out with the SDK and modify it to your heart’s content. Finally, if you like this tutorial and you want to see more, please let us know! Until next time…

Fresh ideas, announcements, and inspiration for your team, delivered weekly.

Subscribe now