Today I’m going to do something I’m typically pretty lousy at, personally, which is finishing what I’ve started. Over the course of my three previous Star Wars themed Plugin Architecture series blog posts, we walked through the development of a search plugin for Confluence. Starting from the cleanest slate possible — a plugin that does nothing at all — we saw how to:

  • add a servlet…
  • make it look visually integrated into Confluence…
  • internationalize it, more or less…
  • find what you’re looking for in the plugin API documentation…
  • create a REST interface to all needed functionality…
  • geek out with Google’s libraries…
  • and make everything awesome with AUI and jQuery…

… but there’s still one big goal I haven’t achieved yet, which is making this plugin work in products other than Confluence. Good news: given all that we’ve done so far, this task turns out to be pretty easy (and consequently, this post will be pretty short). Ready, go.

Step 1: The Omniscient AMPS Plugin

Up to this point, we’ve been using the maven-confluence-plugin to take care of building our source into a plugin JAR, and running that plugin in a local Confluence testing instance. Without getting into too much detail about the guts of the Atlassian Maven Plugin Suite, I’ll just wave my hands a bit and say that maven-confluence-plugin is a thin wrapper around a product-agnostic maven-amps-plugin. Its configuration doesn’t look too different:

1 - amps plugin source.png
Notably, I’ve added product sections in its configuration section for both Confluence and Jira. I’ve also removed one of the last vestiges of the Confluence plugin archetype, which was the provided-scope dependency on Confluence itself, and replaced it with its transitive dependencies that we still need, namely the servlet API and the Google collections library. Not too exciting.
Previously, to run Confluence with our plugin installed, we’d use the atlas-run script from the Plugin SDK, which turns into mvn confluence:run under the covers. We can use the maven-amps-plugin pretty much the same way, specifying the product we want with a system property: mvn amps:run -Dproduct=jira. Again, not too exciting.

Step 2: Fixes for Jira (Slightly More Exciting)

However, one thing we immediately notice upon running Jira is that our handy web-item, that provided the link from Confluence’s “Browse” menu to our servlet page, is missing. So let’s add a new one for Jira:

2 - web-item source.png
It looks pretty much how you’d expect:

3 - web-item rendered.png
… but we’re not out of the woods yet. If you click on the link and try to search now, the REST request will yield an error response, prompting the same awe-inspiring error alert we’ve seen before:

4 - oh noes.png
Oh noes! The issue here, which you can find easily using your Javascript debugger of choice, is that we still think our application’s base URL is http://localhost:1990/confluence. I’d sloppily left that base URL hardcoded in the Javascript, but that’s not going to fly anymore. We need some way to tell the Javascript code where it can find our REST API, and it turns out that we already actually have all the tools we need to do it:

5 - uri fixup source.png
In the first post of this series, we used an I18nResolver instance that the Atlassian Template Renderer conveniently provided by default in its renderer context, to take care of internationalization in the Velocity template. What we’re doing here is simply adding an additional object to that renderer context, that we already had: the SearchResource class. We call buildSelfLink() in our template with a placeholder value of {0}, and finally access and format the final resource URI the same way we did with the search summary text, using AJS.

There are a few potentially subtle tricky bits about that approach, notably the use of decodeURI(). Since I chose to use a placeholder value and format() instead of just string concatenation, the placeholder ends up getting URI-encoded by UriBuilder in buildSelfLink(). We don’t actually want it encoded, so we have to decode it back again.
Does it work?

6 - uri fixup rendered.png

Step 3: Fisheye and Crucible

At this point we’re going to start seeing something funny (for some definition of “funny”) about the different implementations of the Shared Access Layer APIs in each product. I’ve added an additional <product> section in the maven-amps-plugin’s configuration for fecru, and a new web-item like before, and to my surprise nothing blows up. But when I try to search for anything, I get no results. What’s up with that?

Time for amps:debug, which we haven’t really talked about yet. This may be review for many people, but it’s probably worth covering. When you’re trying to debug issues in your Java code, running in an Atlassian application host through the magic of the Plugin SDK, you need to be able to hook your debugger up to that application, which means Maven needs to tell the forked JVM to start itself up with the magic -Xdebug args necessary for remote debugging. Running mvn amps:debug takes care of that for you, and helpfully tells you that it’s listening for debugger socket connections on port 5005.

So, over in my IDE, I set a breakpoint in SearchResource.get() and this is what I see:

7 - param missing.png
Oh yeah. We went to the trouble of including error messages in our search result REST representation, but I never actually bothered displaying them anywhere. Damn my laziness, but hey, we found the problem: Fisheye and Crucible’s implementation of SAL’s SearchProvider API seems to want an extra application parameter passed to it, which Confluence and Jira didn’t need.

Fine, can do:

8 - application param source.png
We inject yet another component, SearchQueryParser (declared with <component-import> as usual in the plugin descriptor), into SearchResource’s constructor. (In my opinion, it should really be called SearchQueryBuilder instead, since it uses the familiar “builder pattern” to construct search queries, but whatever.) We conveniently already have an ApplicationProperties instance that we’d used previously to discover our own base URL, which we can reuse to determine the value for the application parameter, so we feed that to the SearchQueryParser, tell it to build(), and we should be good:

9 - fecru fixed.png

Step 4: Bamboo?

This whole “cross-product search plugin” thing is going smashingly so far. However, I’ll cut to the chase and just tell you right now that we’ve gone as far as we can go with it, at least for the purposes of this tutorial. I was wondering what Bamboo’s implementation of SearchProvider might do, and I got my answer through another fun debugging session:

10 - bamboo fail.png
The answer, evidently, is: nothing.


So, what have we learned today? Well, hopefully the most significant lesson overall is that by avoiding product-specific APIs, and preferring to use functionality provided by SAL and other platform components, it doesn’t take too much to coerce a plugin to work in multiple products. Compared to the previous posts in this series, I didn’t get into too many specific areas of the platform, or design and implementation tricks, but there were a couple worth noting:

  • The maven-amps-plugin: how to run and debug plugins in multiple products.
  • The template-context-item module type: how to inject your own components into the template renderer context, and how that can help with code reuse.

And with that, we’re done! The final code is available for download. As an “exercise for the reader,” as it were, I’d recommend writing some unit and integration tests… Oh wait, I haven’t written my tutorial on best practices for automated plugin testing yet. If you’d like to see that, or any other topic covered, please let us know!.

Plugin Architecture, Episode I ("The Phantom Mess")