In Episodes IV and V of this blog series, we got through a substantial chunk of the implementation for a cross-product search plugin. I’ve personally never been great at estimating the amount of time needed for engineering work, but I’d guess that at the point where we left off, we’d done about three quarters of the work needed to build a Confluence search plugin (ignoring tests, which I sort of thought would be too distracting for this blog series). We’ve got a servlet page that’s nicely integrated with Confluence and a REST service that can perform searches; all we need to do is tie them together. Also, since we’ve been very careful to avoid too many Confluence-specific details throughout, I’m expecting it won’t be too much additional work to take our plugin cross-product (one more post, perhaps?)

Please take note: the user interface we’re about to build is going to be fully web 2.0 buzzword compliant. If you master the following techniques, you will have gained at least ten years worth of highly marketable résumé fodder in less than five minutes. It’s going to be epic. There’s going to be jQuery.

Step 1: Get Resourceful

The first thing we need to do is not actually very résumé-building or sexy at all, but we’ve got to do it. No worries, it won’t take long. If you recall from Episode IV, we have a servlet that’s rendering its content from a Velocity template, and it’s decorating itself with Atlassian headers and footers using SiteMesh, but it doesn’t have any CSS or Javascript of its own. We’re going to be implementing this whole thing in Javascript, and we’ll definitely need to style it up a bit afterwards, so we have to create new CSS and JS files and tell our Velocity template how to reference them.

As always, there’s a plugin module type for that: the Web Resource module. A little bit of background: normally, when you’re referencing style or script resources from a hand-coded HTML file, you’d use <link rel="stylesheet"> and <script> tags. However, in the case of an Atlassian product, since there could be an arbitrary number of plugins influencing the rendering of any given page — each requiring its own style and script resources, located wherever the plugin system happens to put them at runtime — there needs to be a mechanism for keeping all of these resources addressable, organized and non-conflicting. Hence the Web Resource Manager, which does all the necessary juggling. Here’s how it looks:

1 - resources source.png
In the plugin descriptor, I’ve added a <web-resource> module, referencing two new, empty files. I’m also declaring a dependency on “AJS” from the Atlassian User Interface library, because that will provide my jQuery later on. In the Velocity file, I’ve added an important line of code, instructing the web resource manager to include the search-resources (which is fully qualified with the complete plugin key). If some other plugin is also depending on AJS, for example, that’s not a problem — the web resource manager will ensure that it’s only referenced once in the final rendered HTML.

A quick smoketest, using Chrome’s developer tools:

2 - resources loaded.png
Just like that, our resources are being included. If you look closely, note that the relative paths of the web resources aren’t pretty, and aren’t in a form that we could have constructed statically. That’s not a criticism — the resource URLs are an implementation detail that users won’t see — I’m just pointing out that the web resource manager is doing the dirty work of addressing those resources for us.

Step 2: AUI O’Clock

The nice thing about a search page, now that I think about it, is that it doesn’t need to have a complicated user interface. So we can stub this out pretty quickly, just adding a bit of HTML for the entry form and the results, and a bit of Javascript to handle the form submission:

3 - simple aui event source.png
The HTML markup should be pretty clear; the only thing worth mentioning is that I gave pretty long, descriptive IDs to each <div> I anticipate needing to reference from my script, just to make sure they don’t conflict with IDs elsewhere in the final rendered page.

The Javascript, on the other hand, needs a bit of explanation. We’ve got two functions here: one is an anonymous function that is passed as an argument to AJS.toInit(). Remember in Episode V, when I said we were functional programming nerds around here? Well, we’re not the only ones: many Javascript libraries, including jQuery, have functions that take other functions as arguments. This idiom shows up several times in this post, so if you haven’t drunk the functional-style Javascript kool-aid yet, I’ll invite you now to start drinking and keep drinking until it tastes good.

Okay, I got a little sidetracked. The anonymous function is simply a callback that does some setup for us when the page is finished loading. As with any standard event-driven UI code, we have to hook up events to their handlers; in this simple example, hooking up the form button’s “click” event to the “search” handler is all we need to worry about. The handler function is pretty straightforward too: extract the search query from the input field, and stuff it as an <h3> directly into the results. Two important details:

  • The odd-looking AJS.$() mumbo-jumbo is actually just jQuery’s $() function, inside AJS’s namespace, so all of the normal jQuery API applies. Atlassian’s AUI documentation doesn’t have much detail on AJS, because jQuery’s docs are really the best reference.
  • The event.preventDefault() line pops up pretty frequently in some of the plugins I’ve seen, and for good reason: it instructs the browser to “swallow” the event that triggered the handler, preventing it from doing whatever it would normally do. In our case, clicking on a form submit button would cause a page refresh (even though we specified an empty form action), and we need to suppress that.

Another smoketest of this stub code:

4 - simple aui event rendered.png

Step 3: The AJAX / REST Payoff

The stub code is doing what I wanted — just appending the search term to the results container, just to prove to myself that I have my UI elements more or less where I want them, and that nothing is horribly wrong with my event handler — so now I can actually connect the front end to the REST back end from “Episode V.” jQuery makes this very convenient:

5 - ajax search source.png
You tell it the basic details about the HTTP request you’re issuing, and provide handlers for success and error responses. I’ve cut some corners here: first, obviously, in my error handler; and second in my request URL, which is hardcoded. There’s a better way to make your Javascript front end code aware of the REST back end, avoiding hardcoded URLs, but I’ll punt on that until the next post. The meat here, really, is just that when the response comes back, we stuff it into the results container:

6 - ajax search response rendered.png
It’s ugly, of course — we’re not doing any formatting of the JSON response yet, just stringifying it directly — but it basically works, and it’s also nice to see that our REST back end hasn’t mysteriously broken since we last touched it.

Step ?: Getting Sidetracked

I’m going to leave the front end alone for a minute, to deal with a little problem that pops up often in servlet plugins such as this, which is the “oh snap, you’re not logged in” problem. I’ll admit this doesn’t really fit into the overall theme of this post, but it’s an important topic and I couldn’t think of a better place to cover it. Anyway, I couldn’t let this post go without any Java code, so here goes. The problem, in the case of our plugin implementation thus far, looks like this:

7 - authentication ohnoes.png
The user isn’t logged in, and the HTTP response for the search returns an error (remember the @AnonymousAllowed annotation from the last post). The standard way to deal with this is, in most servlet plugins, to have your servlet redirect the user to the login page. SAL once again has a nice utility service to help with that: the LoginUriProvider. Declare a new component-import, inject it in the servlet constructor, and voilà:

8 - login redirect source.png
9 - login redirect rendered.png

Step 4: Parsing JSON

Okay, back to business: we’ve got AJAX working, but we’re not doing anything smart with the JSON content we’re getting back. What we really need to do is iterate over the matches in the response, pull out the fields from each match, and format them nicely. Here, have some jQuery:

10 - omg jquery source.png
There are many ways to do this, and I honestly don’t have nearly enough experience with front end development to say what’s better or worse. I’m just using UPM’s AUI templating style, because it’s what I’m most familiar with, which is basically:

  • Provide a <script type="text/x-template"> for each repeating structure,
  • Reference the “script” by id and extract its HTML content into a variable,
  • For each item (in our case, each search match), clone the template and populate it with the item’s data.

This idiom looks weird the first time you see it, but it’s not really any trickier than Velocity templates or JSP. The main difference (and improvement, I think) is that the overall rendered page structure is driven by the Javascript code — with reusable fragments described in the <script> blocks — rather than having lots of deeply nested #if blocks, #foreach loops, etc.

One other point here: I’m using AJS.$.each() rather than the typical for (i = 0; i < response.matches.length; i++) style. I mentioned before that we typically prefer a functional style here, and this is no exception, but there are a couple more basic reasons why using each() is a good idea: you don’t have to worry about incorrect array indices, and you don’t have to worry about whether you’re iterating over elements in an array or properties of an object.

Okay, here’s what we’ve got so far:

11 - omg jquery rendered.png

Step ?: Getting Sidetracked Again

We have a problem with the content of our REST responses. If you hadn’t noticed it while looking at the JSON, you’ve probably noticed it now that we’re formatting the responses: we’ve got unwanted wiki markup in the excerpt content. I honestly was hoping that Confluence’s implementation of SearchProvider would be a bit friendlier about stripping the wiki markup out of the search results, but it doesn’t. I searched around for a utility somewhere in the Confluence API that would do this for me, but none of them did quite what I needed (or, quite possibly, they did but I wasn’t clever enough to use them correctly).

So here’s another lesson in plugin development: sometimes you write code you’re not proud of. Here’s mine:
12 - ugly hack source.png
I have implemented the awesomest wiki markup parser of all time here, in the form of seven moderately horrifying regular expression replacements (as well as a length limit) that are applied to the search result match excerpts before they’re returned in the REST response. The good news is that it works:

13 - okay.png

Step 5: The Finishing Touches

We’ve basically got everything we need at this point, we just need to make the interface a bit sexier: adding a summary line to the top of the results list, and applying some CSS style to the whole thing. I won’t spend any time discussing the CSS, since I hope it’s self explanatory, but the implementation of the summary line has a surprising number of moving parts that I want to explain:

14 - style and summary source.png

  • I want the summary line to look something like “Found n result(s) in t ms,” where n and t come from the REST response, so that implies some kind of format string.
  • I can use AJS.format() to populate the format string — that’s the easy part — but I want the format string to be internationalized and not hardcoded in the JS.
  • I want to use the I18nResolver to fetch the format string, same as in the first post in this series, but we only have access to it from servlet-land, not from Javascript-land.
  • The trick is to populate a hidden <input> field, using the I18nResolver in the Velocity template, with the internationalized format string. That gets it into the servlet response; once it’s there, I can access it from Javascript.
  • The basic way to reference the hidden <input> is with a standard jQuery selector, something like AJS.$("#search-summary-format"). That’s not bad, but I’m using a helper object that AUI provides, AJS.params, which helps keep everything more organized when you have more of these hidden <input> fields floating around. You keep all of them in one place, a <fieldset> with class “parameters,” and they automatically become properties of the AJS.params object.

I fixed one other minor bug, while I was at it: our servlet page was missing a title this whole time. It just looked like “- Confluence” in the browser, so I added a <title> (and removed the <h1>) to fix it.

Finally, we have a half-respectable search plugin:

15 - booyah.png

Summary

Once again you’ve drunk from the Atlassian fire hose. I actually sort of feel like I need to apologize: I expected this would be a pretty simple article to write, but it turned out to be pretty heavy. We looked at:

  • Web resources: how to reference scripts, stylesheets, images and other static resources from plugins.
  • AJS: some miscellaneous bits and pieces of jQuery, including some very basic event handlers and selectors, generic iterators, AJAX, templating and i18n.
  • Login page redirects: how to get people safely from your servlet to a login page and back again.
  • Dumb hacks: most notably, how you strip wiki markup from a block of text when you’re feeling irritable.

So, we finally have a complete search plugin for Confluence. Okay, it’s not the cross-product search plugin we set out to build yet, but I still need to save some material for “Episode I,” and besides, what we’ve got here is actually pretty damn smooth.

The code for this iteration available for download as usual. If you want to see more of these tutorials, please let us know!

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

Subscribe now

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

Subscribe now