The Whiteboard Pattern for OSGi
Learn about a pattern that a lot of OSGi programmers say is the way to go - and it's certainly one way to go. We'll discuss the whiteboard pattern for OSGi and then discuss some of its strengths and weaknesses, and alternatives.
Introduction
In the first article in this series, we saw how to build OSGi bundles, register services, build simple dependencies on other bundles, and look up services from other OSGi bundles. However, it's not the most efficient way to manage services in a lot of cases. We're going to show a pattern that a lot of OSGi programmers say is the way to go - and it's certainly one way to go. We'll discuss the whiteboard pattern for OSGi and then discuss some of its strengths and weaknesses, and alternatives.
First, let's revisit the code we used in the introductory article for looking up a service:
package service; public interface ServiceLookup { Object getService(String name); } package service.osgi; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceReference; import service.ServiceLookup; public class OSGIServiceLookupImpl implements ServiceLookup { BundleContext ctx; public OSGIServiceLookupImpl(BundleContext ctx) { this.ctx = ctx; } public Object getService(String name) { ServiceReference ref = ctx.getServiceReference(name); return ctx.getService(ref); } }
This code looks up a service by registered name, and returns it. But what happens if the OSGi bundle that holds the service isn't present? After all, one of the features of OSGi is that bundles may or may not be present at any given time; in order for a bundle with a dependency to start, the dependency has to be active, but once that's done, all bets are off. There's a NullPointerException here waiting to happen - nay, asking to happen - nay, begging to happen on bended knee... let's just say we can anticipate having to compensate for dependent services not being around. :)
There are a few ways to manage this, but let's dive straight into a better way of handling this for a lot of cases: the whiteboard pattern.
Basically, the whiteboard pattern is this: a content provider registers itself into the OSGi service registry. A content consumer regularly polls the registry for consumers of the content provider type, and then calls each one in turn. That's fairly simple, but let's show some code to make it obvious, through two examples: one to illustrate the pattern itself, and another to build on our IRC bot example from the first tutorial, by adding a command system where new commands can be installed as OSGi bundles at runtime.
We'll build three bundles, one that monitors the heap of the current JVM, another that offers a clock, and one that displays this information. The first two will depend on the display bundle, because they'll need a common interface. Let's show this interface first, and then build the one of the dependent bundles - then we'll circle back around and implement the rest of the display bundle, and finally, show the heap display bundle so we can have fun activating and deactivating modules.
For the record, this article borrows heavily (and with permission) from BJ Hargrave and Peter Kriens, who have a whitepaper on this pattern published on OSGI.org as "Listeners Considered Harmful: The “Whiteboard” pattern ," including the use of a clock.
The DisplayContentProvider Interface
package display; public interface DisplayContentProvider { /** Designed to return null if no content is present. */ public String getContent(); }
What I did is build this class into a jar, and provide that jar as a build-time dependency for the other modules. It's not important that this module be complete until we get around to deploying the bundles, so we can use the interface as a dependency without concern. We'll need to decide on a symbolic name for the display bundle pretty early - I'm thinking "display-bundle" - so we can build our dependencies properly, but that's a simple problem to solve. (And we just solved it!)
So! Onto our clock bundle!
The Clock Bundle - something Java has needed for a long time, as opposed to int time
It's always been difficult to get the current time from Java. Sure, you could just use new java.util.Date(), or System.currentTimeMillis() if you're looking for distance from the epoch, but ... those are so difficult, and so inelegant, am I right?
Okay, so it's not so hard. But we'll use the clock bundle and the heap display to demonstrate the whiteboard pattern, while understanding that neither of these is particularly hard or interesting in and of themselves. I'm sure readers can think of their own use cases that would be of more interest.
The whole idea behind the whiteboard pattern is that bundles register services under a common name, and a consumer comes along and regularly requests information from each of its providers; as such, what we'll do is first design our ClockContentProvider, then our simple Activator, then our handy-dandy manifest file for OSGi.
package clock.impl; import display.DisplayContentProvider; import java.util.Date; public class ClockContentProvider implements DisplayContentProvider { public String getContent() { return new Date().toString(); } }
As you can see, there's nothing complicated about this at all: if you have an instance of this class, you can call getContent() and get a string representing the current date and time for this JVM. This is even testable without OSGi - imagine that.
Our Activator isn't much more complicated, either:
package clock; import java.util.Hashtable; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; import org.osgi.util.tracker.ServiceTracker; import display.DisplayContentProvider; import clock.impl.ClockContentProvider; public class ClockActivator implements BundleActivator { public void start(BundleContext context) throws Exception { context.registerService( DisplayContentProvider.class.getName(), new ClockContentProvider(), new Hashtable<Object,Object>()); } public void stop(BundleContext context) throws Exception { } }
(Astute readers might pick up the similarity to the RepositoryActivator from the code in the first article in this series. If you didn't pick up on that through the use of maaaagic, it's okay: now you know that this Activator is fairly copy/pasted from the Repository bundle because I told you so.)
Now the manifest for this bundle, to round it out:
Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Clock Display Provider Bundle-SymbolicName: clockdisplay Bundle-Version: 1.0.0 Bundle-Activator: clock.ClockActivator Bundle-Vendor: theserverside.com Import-Package: org.osgi.framework;version="1.3.0", org.osgi.util.tracker;version="1.3.1" Bundle-ClassPath: . Require-Bundle: display-bundle
Note the "Require-Bundle" at the very end - this is how the clock bundle will resolve the dependency on the DisplayContentProvider interface at deployment time.
The Rest of the Display Bundle - where the cool things are
Here's what we do for the whiteboard pattern: we start a thread. That thread will loop, sleeping for one second and then doing something - and that "doing something" will consist of getting all of the available services that implement DisplayContentProvider (and are registered under that name), and then displaying any content they might have. This is going to be almost a perfect copy of the Kriens and Hargrave code, by the way; they did a clear and quality job writing their whiteboard implementation and there's no reason not to stand on the shoulders of giants if the shoulders fit your needs perfectly.
package display; import java.util.Hashtable; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; import org.osgi.util.tracker.ServiceTracker; public class DisplayActivator implements BundleActivator, Runnable { private Thread thread; private ServiceTracker serviceTracker; public void start(BundleContext context) throws Exception { serviceTracker = new ServiceTracker(context, DisplayContentProvider.class.getName(), null); serviceTracker.open(); thread=new Thread(this); thread.start(); } public void stop(BundleContext context) throws Exception { serviceTracker.close(); serviceTracker = null; thread=null; } public synchronized void run() { Thread current=Thread.currentThread(); while(current==thread) { Object[] providers=serviceTracker.getServices(); if(providers!=null && providers.length>0) { for(Object o:providers) { DisplayContentProvider dcp=(DisplayContentProvider)o; String content=dcp.getContent(); if(content!=null) { System.out.println("DISPLAY: "+content); } } } try { wait(15000); } catch(InterruptedException e) { } } } }
Now, notice a few things: first, we're not bothering to register a service. The bundle here doesn't provide any services to any other bundle. It exports an interface (in the manifest file, coming up next) but no services for anyone else to look up.
Now let's look at the run() method. What it's doing is getting all of the services that are registered under the DisplayContentProvider name, and then, for each one, getting any content they may have, and echoing it to the console. (Then it waits for 15 seconds, because otherwise we get flooded by stuff when our content providers are clocks and the like, which never run out of content to show.)
The manifest file is fundamentally simple:
Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Whiteboard Consumer Bundle-SymbolicName: display-bundle Bundle-Version: 1.0.0 Bundle-Activator: display.DisplayActivator Bundle-Vendor: theserverside.com Import-Package: org.osgi.framework;version="1.3.0", org.osgi.util.tracker;version="1.3.1" Export-Package: display;uses:="org.osgi.framework"
The most important things here: Export-Package (without which no other class in the OSGi container can look up the DisplayContentProvider interface), the Bundle-SymbolicName (to which we referred when we built our clock bundle's manifest), and the Bundle-Activator.
Now we have all of the pieces in place. I named these bundles display-bundle.jar and clock-bundle.jar.
$ java -jar org.eclipse.osgi_3.3.2.R33x_v20080105.jar -console osgi> ss
Framework is launched.
id State Bundle 0 ACTIVE org.eclipse.osgi_3.3.2.R33x_v20080105 osgi> install file:///workspaces/osgi/tutorial/clock-bundle.jar Bundle id is 1 osgi> ss
Framework is launched.
id State Bundle 0 ACTIVE org.eclipse.osgi_3.3.2.R33x_v20080105 1 INSTALLED clockdisplay_1.0.0 osgi> start 1 org.osgi.framework.BundleException: The bundle could not be resolved. Reason: Missing Constraint: Require-Bundle: display-bundle; bundle-version="0.0.0" at org.eclipse.osgi.framework.internal.core.BundleHost.startWorker(BundleHost.java:305) at org.eclipse.osgi.framework.internal.core.AbstractBundle.start(AbstractBundle.java:260) [snip!] osgi> install file:///workspaces/osgi/tutorial/display-bundle.jar Bundle id is 2 osgi> ss
Framework is launched.
id State Bundle 0 ACTIVE org.eclipse.osgi_3.3.2.R33x_v20080105 1 INSTALLED clockdisplay_1.0.0 2 INSTALLED display-bundle_1.0.0 osgi> start 2 osgi> start 1 osgi> DISPLAY: Tue Apr 22 13:37:41 EDT 2008 DISPLAY: Tue Apr 22 13:37:56 EDT 2008 stop 1 osgi> ss
Framework is launched.
id State Bundle 0 ACTIVE org.eclipse.osgi_3.3.2.R33x_v20080105 1 RESOLVED clockdisplay_1.0.0 2 ACTIVE display-bundle_1.0.0 osgi>
So what did I do here? First, I started Equinox, of course, and got the "short summary" to show that only the normal default OSGI bundle was installed. I then installed the clock-bundle.jar. Note that it installed correctly, without issue! The only problem was when I tried to start it, without it having a required dependency installed. (I truncated the exception's stack trace to save space.)
I then installed the display bundle, showed that it was installed, and then started it. Note the lack of response; it has no content providers running, so there's no content for it to display. The status is important! The clock bundle is installed and resolved, but not active, so it's not available to the display bundle. I then started the clock bundle. In a few seconds, I started getting content from the display bundle: that's those two timestamps in the output.
I then stopped the clock bundle, and waited - it's hard to show time lapse in articles, but trust me, nothing happened within forty seconds. The clock bundle was still installed, but not started, so the display bundle couldn't find it as a service to provide content.
All of this is well and good, but it's only one content provider. Let's create another content provider, just to show both of them in action. Then we'll jump all the way back to the IRC bot from the first article and start writing commands for it, as OSGi bundles. If having multiple content providers doesn't interest you, go ahead and skip this next section and go straight to the New and Improved IRC Bot.
The Heapsize Display Bundle
The process here is exactly the same as it was for the clock bundle. The only differences will be packages and the specific content returned from the content provider. There are three files we need: the content provider, the activator, and the manifest, in order:
package heap.impl; import display.DisplayContentProvider; import java.util.Date; import java.lang.management.*; public class HeapContentProvider implements DisplayContentProvider { MemoryMXBean memoryBean=ManagementFactory.getMemoryMXBean(); /** Boy, is this output ugly. */ public String getContent() { StringBuilder sb=new StringBuilder(); sb.append("Heap: "); sb.append(memoryBean.getHeapMemoryUsage()); sb.append(" NonHeap: "); sb.append(memoryBean.getNonHeapMemoryUsage()); sb.append(" Objects pending finalization: "); sb.append(memoryBean.getObjectPendingFinalizationCount()); return sb.toString(); } } package heap; import java.util.Hashtable; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; import org.osgi.util.tracker.ServiceTracker; import display.DisplayContentProvider; import heap.impl.HeapContentProvider; /** This is almost a perfect copy of ClockActivator, except s/clock/heap/ */ public class HeapActivator implements BundleActivator { public void start(BundleContext context) throws Exception { context.registerService( DisplayContentProvider.class.getName(), new HeapContentProvider(), new Hashtable<Object,Object>()); } public void stop(BundleContext context) throws Exception { } } Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Heapsize Display Provider Bundle-SymbolicName: heapdisplay Bundle-Version: 1.0.0 Bundle-Activator: heap.HeapActivator Bundle-Vendor: theserverside.com Import-Package: org.osgi.framework;version="1.3.0", org.osgi.util.tracker;version="1.3.1" Bundle-ClassPath: . Require-Bundle: display-bundle
Again, all three files are very, very close to the clock bundle's versions of them - most changes are in names, from Clock to Heap, and the like. Build this into a bundle, install it into your OSGi container of choice, make sure all of the bundles are started - and enjoy. Then you can uninstall them, because the output is really ugly. :)
An Extensible IRC Bot
In the first article, we built a truly primitive infobot. It responded to "~put factoid_name factoid" and "~get factoid_name," where the factoid names were basically XPath expressions. (What's more, with the XUL repository, they're truly XPath expressions: you could put in "~get /fact/*" and get the first of any number of nodes, depending on how the XPath engine reported nodes back.) However, fixing the infobot in any way meant uninstalling it and re-installing it, and presumably restarting the darned thing. That's no fun. We should be able to have a deployable bot where new commands can be installed at any point.
This is perfect for the whiteboard pattern. We can install a pircbot implementation, and have the onMessage() method look for implementations of IRCCommand, passing the message to each command in turn (possibly stopping when the command is consumed.) If a command is broken, we can uninstall the command, fix it, and reinstall - all transparent to the IRC bot. Even our factoids can be implemented this way, so our IRC Bot turns into a whiteboard consumer, and can do anything we can think of to make it do.
So where do we start? Well, let's rewrite our ircbot. We'll touch on the Activator, the IRCBot itself, and the manifest, because all three will change. The Activator no longer has to provide a way to get the services and can be the bot itself, which implements the whiteboard, the manifest file now has no external dependencies. We'll also have to add a new interface (for the commands to implement) and a IRCMessage to communicate with them consistently.
So let's get coding! First, the IRCMessage class and the IRCCommand interface:
package ebot; import java.util.Date; public class IRCMessage { String channel; String sender; String login; String hostname; String message; Date created; boolean processed; public IRCMessage() { created=new Date(); } // mutators and accessors not included for brevity. } package ebot; public interface IRCCommand { IRCMessage handleCommand(Ebot bot, IRCMessage message); }
Scary stuff, huh? Basically, in Pircbot, every channel message has those Strings defined; the rest is stuff we can use for logging, or for indicating that messages were consumed (or not to be further consumed). The IRCCommand interface returns an IRCMessage itself; if the whiteboard gets a non-null IRCMessage, it will send the IRCMessage's message to the channel indicated by the channel attribute.
The Activator can serve as the IRC Bot itself - which makes a lot of things quite easy. The start(BundleContext) initializes the connection to the IRC server and builds the service tracker; the onMessage() is where the whiteboard magic happens. It looks for any implementations of IRCCommand registered in the OSGi container, and passes the IRC message to each of them. If the command returns a non-null IRCMessage, it sends a message back out. For the commands to be polite, they should respect (and use) the "IRCMessage.processed" attribute. Note also that we're not trying to compensate for long-running command implementations.
So let's look at the code, because nobody wants to read sentence after sentence about what the code does without at least seeing the code. That's dull, uninteresting, and ineffective. And I'll shut up now:
package ebot.impl; import ebot.IRCCommand; import ebot.IRCMessage; import org.jibble.pircbot.PircBot; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; import org.osgi.util.tracker.ServiceTracker; public class Ebot extends PircBot implements BundleActivator { public Ebot() { super(); setName("EBot"); } private Thread thread; private ServiceTracker serviceTracker; public void start(BundleContext context) throws Exception { serviceTracker = new ServiceTracker(context, IRCCommand.class.getName(), null); serviceTracker.open(); setVerbose(true); connect("irc.freenode.net"); joinChannel("#pircbot"); } public void stop(BundleContext context) throws Exception { serviceTracker.close(); serviceTracker = null; } @Override protected void onMessage(String channel, String sender, String login, String hostname, String message) { IRCMessage packet = new IRCMessage(); packet.setChannel(channel); packet.setSender(sender); packet.setLogin(login); packet.setHostname(hostname); packet.setMessage(message); Object[] providers = serviceTracker.getServices(); if (providers != null && providers.length > 0) { for (Object o : providers) { IRCCommand command = (IRCCommand) o; IRCMessage newMessage=command.handleCommand(packet); if(newMessage!=null) { sendMessage(newMessage.getChannel(), newMessage.getMessage()); } } } } }
Nothing earth-shattering, is it? Here's the manifest for the bundle:
Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: The Extensible Bot Bundle-SymbolicName: ebot-bundle Bundle-Version: 1.0.0 Bundle-Activator: ebot.impl.Ebot Bundle-Vendor: theserverside.com Import-Package: org.osgi.framework;version="1.3.0", org.osgi.util.tracker;version="1.3.1" Export-Package: ebot;uses:="org.osgi.framework" Bundle-ClassPath: pircbot.jar,.
We now have something that can connect to an IRC network and - potentially - respond to messages. The next obvious thing to do is to build some commands!
The Most Valuable IRC Bot Command Ever
Well, it's the most valid command besides "~quit, you stupid bot," let's say. What we'll do is build a command that will show the current date in the channel from which a "~date" message is received. In other words, if you're in #pircbot and you type in "~date" , you'll get the bot's current date and time. (Golly, this article is fixated on dates and times, isn't it?)
So as usual, we'll have an activator and a manifest. We'll use the Activator itself to implement the command. First, our manifest:
Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: IRCBot Date Command Bundle-SymbolicName: ircdate Bundle-Version: 1.0.0 Bundle-Activator: irccommand.Command Bundle-Vendor: theserverside.com Import-Package: org.osgi.framework;version="1.3.0", org.osgi.util.tracker;version="1.3.1" Require-Bundle: ebot-bundle Bundle-ClassPath: .
Now for the Activator/Command:
package irccommand; import ebot.IRCCommand; import ebot.IRCMessage; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; import org.osgi.util.tracker.ServiceTracker; import java.util.Date; import java.util.Hashtable; public class Command implements IRCCommand, BundleActivator { public void start(BundleContext context) throws Exception { context.registerService( IRCCommand.class.getName(), this, new Hashtable<Object, Object>()); } public void stop(BundleContext context) throws Exception { } public IRCMessage handleCommand(IRCMessage ircMessage) { if (!ircMessage.isProcessed() && "~date".equals(ircMessage.getMessage())) { // we need to mark this as "processed" for politeness' sake ircMessage.setProcessed(true); // we build a new message IRCMessage message = new IRCMessage(); message.setChannel(ircMessage.getChannel()); message.setMessage(new Date().toString()); return message; } return null; } }
One oddity: I called the command implementation "Command." Why, you might be asking yourself? ("Why" to a whole lot of things, actually, which is understandable, but bear with me.) The main reason is so I can cut and paste the entire bundle's source tree into a new directory, modify the Command and the manifest's symbolic name, and have a new command in no time.
So what does this do at runtime? Not much, really. When you install and start the ebot-bundle, it'll connect to Freenode and join #pircbot, and... that's it. You can try to interact with it all you want, but it won't do anything; it has no commands! Now, if you install the ircdate-bundle and start it too, when a user types in "~date" to the IRC channel, it'll immediately spit back a date. Stop the ircdate-bundle and it no longer responds to "~date" at all.
This is a useless bot, to be sure. But imagine a ircfactoid bundle, where handleCommand() looked for "~get" or "~set," and you can see where the power is; that ircfactoid bundle could have a dependency on a repository, so you could make it query anything you wanted to install.