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
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
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:
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
(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:
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
I anticipate needing to reference from my script, just to make sure they don’t conflict with IDs elsewhere in the final rendered page.
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
directly into the results. Two important details:
- The odd-looking
mumbo-jumbo is actually just jQuery’s1$()
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.
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:
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:
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:
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
. Declare a new
, inject it in the servlet constructor, and voilà:
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:
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
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.
blocks — rather than having lots of deeply nested
One other point here: I’m using
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
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:
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
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:
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:
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:
- 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
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
- The trick is to populate a hidden
field, using the1I18nResolver
- The basic way to reference the hidden
is with a standard jQuery selector, something like1AJS.$("#search-summary-format")
. That’s not bad, but I’m using a helper object that AUI provides,1AJS.params
, which helps keep everything more organized when you have more of these hidden1<input>
fields floating around. You keep all of them in one place, a1<fieldset>
with class “parameters,” and they automatically become properties of the1AJS.params
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
(and removed the
) to fix it.
Finally, we have a half-respectable search plugin:
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.