OSGi for Beginners
As a non-OSGi advocate, I would like to take some time to try to explain OSGi to the people who don't know about it – a novel idea, apparently – along with some example code to help people get started.
Introduction
The Ig Nobel prize ceremony features a few very interesting concepts, apart from the Ig Nobel prizes themselves. One of them is a chance for the literati to explain a concept in twenty-four seconds, followed by a seven word summary.
Read Peter Kriens article on why OSGi trumps Spring IoC for de-coupling and control
This is a brilliant idea, and one every project should consider as a challenge to fulfill.
OSGi and Java modularity has been mentioned quite a bit in the industry lately, with Equinox becoming a top-level project for Eclipse, Felix being used as a container for Sling and Glassfish V3, and Spring-Modules being released. That said, a lot of people who are unfamiliar with OSGi ... are still unfamiliar with it, no matter how much the people who use OSGi regularly know about it.
So I, as a non-OSGi advocate, would like to take some time to try to explain OSGi to the people who don't know about it – a novel idea, apparently – along with some example code to help people get started. Again, I'm not an OSGi advocate (well, not from the standpoint of being part of OSGi.) I'm just someone who thinks OSGi has viability, except that it's got really poor documentation unless you're so good at it that you don't need the introductory materials.
First, I'll give you the seven-word explanation, then the twenty-four second explanation (transcripted), and then I'll try to explain the whole thing.
The Seven-Word and Twenty-Four-Second explanations of OSGi
OSGi is a component framework for Java.
The twenty-four second explanation: OSGi is a framework for Java in which units of resources called bundles can be installed. Bundles can export services or run processes, and have their dependencies managed, such that a bundle can be expected to have its requirements managed by the container. Each bundle can also have its own internal classpath, so that it can serve as an independent unit, should that be desireable. All of this is standardized such that any valid OSGi bundle can theoretically be installed in any valid OSGi container.
Rats. Twenty-seven seconds, no matter how fast I run through - I just can't talk quickly enough. What's worse, the explanation doesn't explain why one wants a module system in the first place.
Why a Module System?
Module systems provide version support for distributed bundles (where "bundle" goes way beyond "OSGi bundle" - I'm using the term to refer to any application.) Dependency hell is also an issue; lifecycle is ... interesting.
All of these things are important; versioning still hasn't made it into web services, EJB versioning is enforced via JNDI, but few use it (nobody in the wild that I know of), jar dependencies are managed normally with parallel jar deployments (except for JCA and WARs, both of which have different ways of managing dependencies).
Java EE has solutions, although not necessarily good ones: WARs and JCA can contain jar files, EJB jars can refer to other jar files through their manifests, and of course app servers can provide a higher-level class repository; versioning is provided through JNDI as long as you're not using different versions of the same web app or web services. Lifecycle exists for webapps (load-on-startup servlets, context listeners) and JCA, but EJB 3.1 might have a lifecycle mechanism - it's not sure yet.
And we all know that Java EE is a hammer that fits everything.
OSGi and JSR-277 are attempts to standardize module deployments for Java, without forcing a Java EE mindset, and without Java EE's weaknesses regarding dependencies and versioning and - for that matter - lifecycle. Since this is an OSGi article and not a module article, we'll focus on OSGi...
Getting Started with OSGi
Basically, running OSGi is very simple, and fundamentally uninteresting: one grabs one of the OSGi container implementations (Equinox, Felix, Knopflerfish, and ProSyst, among others) and executes the container's boot process, much like one runs a Java EE server. Like Java EE, each container has a different startup environment and slightly different capabilities; check your container of choice for details and options. For the sake of clarity, we'll use Equinox in this article.
Equinox is an OSGi container, of course, and it can be downloaded from http://download.eclipse.org/eclipse/equinox/. The downloaded bundle is a ZIP file that extracts into a directory called "eclipse, " which shouldn't be surprising: Equinox is the OSGi container Eclipse uses internally. (I'll refer to this directory - "/eclipse," the top-level directory contained in the Equinox distribution - as $EQUINOX.) As documented on the Equinox Quickstart page , looking in $EQUINOX/plugins shows a pretty large collection of jars:
/opt/tools/eclipse/plugins> ls javax.servlet.jsp_2.0.0.v200706191603.jar org. eclipse.equinox.jsp.jasper_1.0.1.R33x_v20070816.jar javax.servlet_2.4.0. v200706111738.jar org.eclipse.equinox.launcher_1.0.1.R33x_v20070828.jar org. apache.commons.el_1.0.0.v200706111724.jar org.eclipse.equinox.launcher_1.0.1. R33x_v20080118.jar org.apache.commons.logging_1.0.4.v200706111724.jar org. eclipse.equinox.log_1.0.100.v20070226.jar org.apache.jasper_5.5.17.v200706111724. jar org.eclipse.equinox.metatype_1.0.0.v20070226.jar org.eclipse.equinox.app_1. 0.1.R33x_v20070828.jar org.eclipse.equinox.preferences_3.2.100.v20070522.jar org. eclipse.equinox.common_3.3.0.v20070426.jar org.eclipse.equinox.preferences_3. 2.101.R33x_v20080117.jar org.eclipse.equinox.device_1.0.0.v20070226.jar org. eclipse.equinox.registry_3.3.1.R33x_v20070802.jar org.eclipse.equinox.event_1. 0.100.v20070516.jar org.eclipse.equinox.servletbridge_1.0.1.R33x_v20070816.jar org.eclipse.equinox.http.jetty_1.0.1.R33x_v20070816.jar org.eclipse.equinox. source_3.3.1.R33x_r20070918-7n7LECgEKVsLIM1aGBO4b00 org.eclipse.equinox.http. registry_1.0.0.v20070608.jar org.eclipse.equinox.source_3.3.1.R33x_r20070918-7 n7LEClEIdwb-bbP_z--EYAO org.eclipse.equinox.http.registry_1.0.1.R33x_v20071231. jar org.eclipse.equinox.useradmin_1.0.0.v20070226.jar org.eclipse.equinox.http. servlet_1.0.1.R33x_v20070816.jar org.eclipse.osgi.services_3.1.200.v20070605.jar org.eclipse.equinox.http.servletbridge_1.0.0.v20070523.jar org.eclipse.osgi. util_3.1.200.v20070605.jar org.eclipse.equinox.http_1.0.100.v20070423.jar org. eclipse.osgi_3.3.1.R33x_v20070828.jar org.eclipse.equinox.http_1.0.101. R33x_v20071016.jar org.eclipse.osgi_3.3.2.R33x_v20080105.jar org.eclipse.equinox. jsp.jasper.registry_1.0.0.v20070607.jar org.mortbay.jetty_5.1.11.v200706111724. jar
Starting Equinox is simple: in this directory, execute java -jar org.eclipse.osgi_3.3.2.R33x_v20080105.jar -console (in a console window), and you'll soon see a prompt:
osgi>
This is the OSGi console for Equinox. From it, you can install new bundles, start them, stop them, uninstall them, check their dependencies, check registered services, or any number of other things. The first command you should probably try is "ss," for "short status." For a new installation, the interaction looks like this:
osgi> ss Framework is launched. id State Bundle 0 ACTIVE org.eclipse.osgi_3. 3.2.R33x_v20080105 osgi>
This shows that the container is running, with a single bundle installed. This bundle has an id of "0," which is going to be pretty relevant, because you use that id to control the bundle's lifecycle.
So what's a bundle good for? Well, a bundle provides a lifecycle and exported services, as our friendly twenty-seven second summary mentioned.
A Simple Repository to put into a Bundle
So let's create a bundle. I'd like a way to connect to a repository of information. My initial interface should look something like this:
package repository; public interface RepositoryService { Node put(String path, Node content); Node get(String path); }
It's not much, but it's something to get started: I can use this to put information in, or get information out. My Node class is a simple POJO, which looks something like this:
package repository; import java.util.ArrayList; import java.util.Date; import java.util.List; public class Node { Date created = new Date(); String source = " unknown"; String author = "unknown"; Listcontents = new ArrayList< String>(); String path; @Override public String toString() { String s = " node: path=" + getPath() + ", author=" + getAuthor() + ", created=" + getCreated () + ", source=" + getSource() + ", data=["; String separator = ""; for (String d : getContents()) { s += separator + d; separator = ","; } s += "]"; return s; } public Node() { } public Node(String content) { getContents().add(content); } public Node(String content, String context) { this(content); setSource(context); } // .. accessors and mutators go here. }
This is a service that can be implemented and tested independently from OSGi; we have nothing that has anything to do with OSGi yet. With that, we probably should go ahead and have an implementation. I originally coded this as a Map:
import java.util.HashMap; import java.util.Map; import repository.Node; import repository.RepositoryService; public class MapRepositoryService implements RepositoryService { Map<String, Node> data=new HashMap<String, Node> (); public Node put(String path, Node content) { return data.put(path, content); } public Node get(String path) { return data.get(path); } }
However, a Map provides poor search facilities, and any hierarchy of information is accidental. I'd like to use a DOM for this, instead. So I'll introduce a dependency on XOM for now, and add a new repository service implementation:
package repository.impl; import java.util.Date; import nu.xom.Attribute; import nu.xom.Document; import nu.xom.Element; import nu.xom.Elements; import nu.xom. Nodes; import nu.xom.ParentNode; import repository.Node; import repository. RepositoryService; public class XMLRepositoryService implements RepositoryService { Document document; Element data; public XMLRepositoryService () { data = new Element("data"); document = new Document(data); } Node toNode(nu. xom.Element node) { if (node.getAttribute("source") == null) { return null; } Node n = new Node(); n.setAuthor(node.getAttributeValue("author")); n.setCreated (new Date(node.getAttributeValue("created"))); n.setSource(node. getAttributeValue("source")); Elements e = node.getChildElements("contents"); n. setPath(node.getAttributeValue("path")); for (int i = 0; i < e.size(); i++) { Element elt = e.get(i); for (int i1 = 0; i1 < elt.getChildCount(); i1++) { n.getContents().add(elt.getChild(i1).getValue()); } } return n; } nu.xom.Element getElement(String path) { while (!path.startsWith("//")) { path = "/" + path; } while (path.endsWith("/")) { path = path.substring(0, path.length() - 1); } Nodes nodes = document.query(path); if (nodes.size() > 0) { return (Element) nodes.get(0); } return null; } public Node get(String path) { Element e = getElement(path); if (e != null) { return toNode(e); } return null; } public Node put(String path, Node content) { Element oldElt = getElement(path); if ( oldElt != null) { // need to remove this node! ParentNode p = oldElt.getParent(); p.removeChild(oldElt); } StringBuilder pathBuilder=new StringBuilder("/"); String[] tree = path.split("/"); Element node = data; for (String t : tree) { if (t.length() != 0) { Element child = node.getFirstChildElement(t); if (child == null) { //System.err.println("creating new "+t); child = new Element(t); node. appendChild(child); } pathBuilder.append('/'); pathBuilder.append(t); node = child; } } node.addAttribute(new Attribute("created", content.getCreated() . toString())); node.addAttribute(new Attribute("source", content.getSource())); node.addAttribute(new Attribute("author", content.getAuthor())); content.setPath (pathBuilder.toString()); node.addAttribute(new Attribute("path", content. getPath())); Element contents = new Element("contents"); node.appendChild( contents); for (String c : content.getContents()) { Element e = new Element(" content"); e.appendChild(c); contents.appendChild(e); } //System.out.println( data.toXML()); return null; } public static void main(String[] args) { XMLRepositoryService s = new XMLRepositoryService(); s.put("/foo/bar/baz", new Node("stuff")); Node n = new Node("bletch"); n.setAuthor("jottinger"); s.put("/ foo/bar/bletch", n); System.out.println(s.get("foo/bar/baz/")); System.out. println(s.get("//foo/bar/baz")); System.out.println(s.get("//foo/bar/bletch")); System.out.println(s.get("foo/bar/")); System.out.println(s.get("//*[@author=' jottinger']")); } }
It's still far from perfect, but it's a good start. However, we still haven't done anything with respect to OSGi - we only have a slightly usable repository class. Let's look at how OSGi modules are built.
Sidetrack: Building an OSGi bundle with a Dependency
An OSGi module is a .jar file. In this, it follows the standard .jar file specification , except it has a set of specific requirements in its manifest file .
Let's create a simple module, first. This will simple display a message on startup and shutdown, and along the way will include a .jar file as an internal dependency. This jar file will not be visible to any other bundle - it's just a resource for our tutorial bundle. However, this is a fairly simple and common requirement that is poorly documented. (Well, it's poorly documented for me - I looked for a simple example and couldn't find one.) The dependency will be in a jar, baselib.jar, and it will be one class: baselib.BaseService.
package baselib; import java.util.logging.Logger; public class BaseService { Logger log=Logger.getLogger(this.getClass().getName()); public void sayHello() { log.info("Hello, world!"); } }
Complex, earth-shattering stuff here, to be sure. What we need to do is build this file into a .jar of its very own, one I'll call baselib.jar.
Now, an OSGi bundle requires an "activator," a class that manages the lifecycle of the bundle. An activator implements the org.osgi.framework.BundleActivator interface, which means two methods: start(BundleContext) and stop(BundleContext). These lifecycle methods are where a bundle can register services or start processes, but for us, it's far simpler:
package tutorial; import baselib.BaseService; import org.osgi.framework. BundleActivator; import org.osgi.framework.BundleContext; import java.util. logging.Logger; public class TutorialActivator implements BundleActivator { Logger log=Logger.getLogger(this.getClass().getName()); public void start( BundleContext bc) { log.info("started"); new BaseService().sayHello(); } public void stop(BundleContext bc) { log.info("stopped."); } }
We can't just stuff this into a jar file and have it work for us, sadly ( Spring-OSGi can help here, but it's far beyond scope for this article.) We need to build a tutorialbundle.jar with a specific set of files and a specific structure. First off, baselib.jar must be in the root directory of our new jar. As well, we need a MANIFEST.MF that includes some OSGi configuration and startup information:
Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-SymbolicName: com. theserverside.tutorial.osgi.TutorialBundle Bundle-Version: 1 Bundle-Activator: tutorial.TutorialActivator Import-Package: org.osgi.framework;version="1.3.0" Bundle-ClassPath: .,baselib.jar
This manifest works on the assumption that our jar looks like this:
$ jar tvf tutorialbundle.jar 0 Thu Apr 17 11:57:14 EDT 2008 META-INF/ 391 Thu Apr 17 11:57:12 EDT 2008 META-INF/MANIFEST.MF 0 Thu Apr 17 11:29:56 EDT 2008 tutorial/ 714 Thu Apr 17 11:51:02 EDT 2008 tutorial/TutorialActivator.class 902 Thu Apr 17 11:15:28 EDT 2008 baselib.jar
The most important things in our manifest are the imported packages (just the OSGi base framework in this case), the activator class name, and the bundle classpath - a comma-separated set of resources in the jar file. If we can duplicate this structure, then we're ready to install this bundle and run it in Equinox.
$ 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/tutorialbundle. jar Bundle id is 4 osgi> start 4 Apr 17, 2008 11:57:29 AM tutorial. TutorialActivator start INFO: started Apr 17, 2008 11:57:29 AM baselib. BaseService sayHello INFO: Hello, world! osgi> stop 4 Apr 17, 2008 1:29:25 PM tutorial.TutorialActivator stop INFO: stopped. osgi>
Now we can see how to build a bundle, and how to deploy it with dependent jars (a requirement, since our repository class above relies on XOM.)
Building our Repository Bundle
Now we can work on our repository bundle, which adds some additional functionality in the Activator: it registers the repository as a service and exports it, so that other bundles can use our repository to look stuff up.
package repository; import java.util.Hashtable; import org.osgi.framework. BundleActivator; import org.osgi.framework.BundleContext; import org.osgi.util. tracker.ServiceTracker; import repository.impl.XMLRepositoryService; public class Activator implements BundleActivator { /** * @see org.osgi.framework. BundleActivator#start(org.osgi.framework.BundleContext) */ public void start( BundleContext context) throws Exception { // register the service context. registerService( RepositoryService.class.getName(), new XMLRepositoryService(), new Hashtable<Object,Object>()); // create a tracker and track the log service ServiceTracker repositoryServiceTracker = new ServiceTracker(context, RepositoryService.class.getName(), null); repositoryServiceTracker.open(); // grab the service RepositoryService repositoryService = (RepositoryService) repositoryServiceTracker.getService(); System.err.println("RepositoryService Activated"); } /* * (non-Javadoc) * @see org.osgi.framework.BundleActivator#stop (org.osgi.framework.BundleContext) */ public void stop(BundleContext context) throws Exception { // close the service tracker System.err.println(" RepositoryService Deactivated"); } }
Note that this hardcodes the use of our XMLRepositoryService, above, which is a little icky. We could use Spring, or the Service Provider Interface, or an environment variable, or something - even an OSGi preference service - to make this a runtime decision, but that's outside of our scope for this article. Let's get this service deployed and we'll show you how to use it from another bundle, which will itself open the door for other options.
Following our first simple bundle, the relevant information for our repository bundle consists of the manifest file and the directory structure. Here's the directory structure:
$ jar tvf repositorybundle.jar 0 Thu Apr 17 14:08:46 EDT 2008 META-INF/ 553 Thu Apr 17 14:08:44 EDT 2008 META-INF/MANIFEST.MF 0 Thu Apr 17 13:57:12 EDT 2008 repository/ 0 Thu Apr 17 13:57:12 EDT 2008 repository/impl/ 1383 Thu Apr 17 13: 57:12 EDT 2008 repository/Activator.class 2095 Thu Apr 17 13:57:12 EDT 2008 repository/Node.class 205 Thu Apr 17 13:57:12 EDT 2008 repository/ RepositoryService.class 694 Thu Apr 17 13:57:12 EDT 2008 repository/impl/ MapRepositoryService.class 3823 Thu Apr 17 13:57:12 EDT 2008 repository/impl/ XMLRepositoryService.class 895924 Thu Apr 17 14:03:40 EDT 2008 xerces-2.4.0.jar 109318 Thu Apr 17 14:05:08 EDT 2008 xml-apis-1.0.b2.jar 431568 Thu Apr 17 13:54: 06 EDT 2008 xom-1.1.jar And the manifest file: Manifest-Version: 1.0 Bundle- ManifestVersion: 2 Bundle-Name: Repository Plug-in Bundle-SymbolicName: repository Bundle-Version: 1.0.0 Bundle-Activator: repository.Activator Bundle- Vendor: theserverside.com Import-Package: org.osgi.framework;version="1.3.0", org.osgi.util.tracker;version="1.3.1" Export-Package: repository;uses:="org.osgi. framework" Bundle-ClassPath: .,xom-1.1.jar,xerces-2.4.0.jar,xml-apis-1.0.b2.jar
So what did we just do? We built a jar, using the Activator implementation we offered earlier, and this has a number of dependencies: XOM, an implementation of Xerces (because of XOM), and the ServiceTracker API.
We are also doing one other interesting thing: we're exporting the repository package. This means that other bundles in our OSGi container can import those packages, and look up specific services by name (in this case, the repository is registered under the name of the RepositoryService interface, or "repository.RepositoryService".) We can install this bundle now and start it:
osgi> install file:///workspaces/osgi/tutorial/repositorybundle.jar Bundle id is 11 osgi> start 11 RepositoryService Activated osgi>
This doesn't look all that exciting yet, but we've actually got a working OSGi infrastructure we can leverage now. It should be noted that we're putting our interface in with our implementation. In a perfect world, RepositoryService would be in its own jar, so we can separate the interface and its implementation. This isn't hard, even from a bundle perspective; in the activator for the interface bundle, you'd just do nothing, and in the implementing bundle, you'd import the interface from the other (interface) bundle. We're just not doing it here because it creates a maze of twisty little passages, all alike, and that slows everyone down.
Using our OSGi Bundle from another Bundle
The last step is to build yet another bundle - but this one will look up the repository service and use it.
Let's show the Bundle Activator first. It's really quite simple, and entirely nonfunctional: when the bundle starts, it looks up the RepositoryService, and stores something into the service just to make sure the repository has something in it. It uses the stop() mechanism to actually look something up in the repository and display it on console. This isn't exactly glamorous work, but it's enough to show the process in motion:
package testrepouser; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceReference; import java.util.logging.Logger; import repository.Node; import repository.RepositoryService; public class SampleActivator implements BundleActivator { Logger log=Logger.getLogger(this.getClass().getName()); /* * (non-Javadoc) * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) */ public void start(BundleContext context) throws Exception { ServiceReference ref = context.getServiceReference(RepositoryService.class.getName()); RepositoryService lookup = (RepositoryService) context.getService(ref); Node testNode=new Node("this is some content"); lookup.put("/foo/bar/baz", testNode); log.info("/foo/bar/baz stored."); } /* * (non-Javadoc) * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) */ public void stop(BundleContext context) throws Exception { ServiceReference ref = context.getServiceReference(RepositoryService.class.getName()); RepositoryService lookup = (RepositoryService) context.getService(ref); log.info(lookup.get("///baz")); } }
The MANIFEST.MF file looks like this: Manifest-Version: 1.0 Bundle- ManifestVersion: 2 Bundle-Name: Repository Sample Plug-in Bundle-SymbolicName: samplerepouser Bundle-Version: 1.0.0 Bundle-Activator: sample.SampleActivator Bundle-Vendor: theserverside.com Import-Package: org.osgi.framework;version=" 1.3.0" Require-Bundle: repository
The last line stands out. If you look at the RepositoryService's manifest, the symbolic name there is... "repository." Here, we're saying that this bundle should be able to access exported classes from whatever bundle is referred to here - in other words, since our repository bundle exports the "repository" package, our SampleActivator can import the repository classes directly from the repository bundle instead of having to package them itself.
We build our sample.jar bundle, which should look like this:
$ jar tvf ../samplebundle.jar 0 Thu Apr 17 14:47:48 EDT 2008 META-INF/ 421 Thu Apr 17 14:47:46 EDT 2008 META-INF/MANIFEST.MF 0 Thu Apr 17 14:47:48 EDT 2008 sample/ 1270 Thu Apr 17 14:47:48 EDT 2008 sample/SampleActivator.class
Note the simplicity here: there's nothing in the bundle except our activator. There are no service implementations, not even the interface.
We can install the sample bundle, start it, then stop it, to show everything happening:
osgi> install file:///workspaces/osgi/tutorial/samplebundle.jar Bundle id is 17 osgi> start 17 Apr 17, 2008 2:49:07 PM sample.SampleActivator start INFO: /foo/bar/baz stored. osgi> stop 17 Apr 17, 2008 2:49:09 PM sample. SampleActivator stop INFO: node: path=//foo/bar/baz, author=unknown, created=Thu Apr 17 14:49:07 EDT 2008, source=unknown, data=[this is some content] osgi>
Running our Bundles in another OSGi Container
One of the strengths of OSGi is, of course, the "platform neutrality" of the containers, much as Java EE modules are supposedly deployable to any compatible containers. Let's take just a moment to show our bundles described above being deployed in Felix - Apache's OSGi container - just to show how the bundles look in another container. Felix asks you to name a configuration, so it can reload it if you use the name again - we'll call ours "tutorial01" since this is, after all, a tutorial.
$ java -jar bin/felix.jar Welcome to Felix. ================= Enter profile name: tutorial01 DEBUG: WIRE: 1.0 -> org.ungoverned.osgi.service.shell -> 1.0 DEBUG: WIRE: 1.0 -> org.osgi.service.startlevel -> 0 DEBUG: WIRE: 1.0 -> org.apache.felix.shell -> 1.0 DEBUG: WIRE: 1.0 -> org.osgi.framework -> 0 DEBUG: WIRE: 1.0 -> org.osgi.service.packageadmin -> 0 DEBUG: WIRE: 2.0 -> org.apache.felix.shell -> 1.0 DEBUG: WIRE: 2.0 -> org.osgi. framework -> 0 DEBUG: WIRE: 3.0 -> org.osgi.framework -> 0 DEBUG: WIRE: 3.0 -> org.osgi.service.obr -> 3.0 DEBUG: WIRE: 3.0 -> org. apache.felix.shell -> 1.0 -> install file:///workspaces/osgi/tutorial/ tutorialbundle.jar Bundle ID: 7 -> start 7 DEBUG: WIRE: 7.0 -> org.osgi. framework -> 0 Apr 18, 2008 10:46:37 AM tutorial.TutorialActivator start INFO: started Apr 18, 2008 10:46:37 AM baselib.BaseService sayHello INFO: Hello, world! -> install file:///workspaces/osgi/tutorial/repositorybundle.jar Bundle ID: 8 -> start 8 DEBUG: WIRE: 8.0 -> org.osgi.util.tracker -> 0 DEBUG: WIRE: 8.0 -> org.osgi.framework -> 0 RepositoryService Activated -> install file:///workspaces/osgi/tutorial/samplebundle.jar Bundle ID: 9 -> start 9 DEBUG: WIRE: 9.0 -> org.osgi.framework -> 0 DEBUG: WIRE: 9.0 -> module;bundle-symbolic-name="repository";bundle-version= "1.0.0" -> 8.0 Apr 18, 2008 10:47:08 AM sample.SampleActivator start INFO: / foo/bar/baz stored. -> stop 9 Apr 18, 2008 10:47:09 AM sample. SampleActivator stop INFO: node: path=//foo/bar/baz, author=unknown, created=Fri Apr 18 10:47:07 EDT 2008, source=unknown, data=[this is some content] -> shutdown -> RepositoryService Deactivated Apr 18, 2008 10:47:12 AM tutorial. TutorialActivator stop INFO: stopped.
An Actual Application, sort of: an IRC Bot
It's all very easy to see the sample repository working as a test - but a test isn't very interesting. Let's take our repository one step further, and hook it into an IRC bot. Our IRC bot will use PircBot , mostly because of its trivial API, will join only one channel on one IRC network (irc.freenode. net's, "#pircbot" channel), and will respond to only two external commands: ~set and ~get. ~set will take a path and some text, and will add the text to that path; ~get will retrieve information from that path. As such, it'll be a sort of painfully simple and uncorrectable infobot ; it'll be left as an exercise to the reader to make it more functional.
One of the first things we want to do is create a generic way to look up services. This is surely not the best way to do this! There are a lot of different patterns for this sort of thing; this is simply a way to get started quickly, not well. We'll create a ServiceLookup class, and then an implementation of this ServiceLookup for OSGi.
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); } }
Using the OSGIServiceLookupImpl is simple, once it's constructed with the Activator's BundleContext:
package ircbot; import org.jibble.pircbot.IrcException; import org.jibble. pircbot.NickAlreadyInUseException; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; import service.osgi. OSGIServiceLookupImpl; import java.io.IOException; public class BotActivator implements BundleActivator { IRCBot bot = null; public void start(final BundleContext context) throws Exception { try { bot = new IRCBot(new OSGIServiceLookupImpl(context)); bot.setVerbose(true); bot.connect("irc.freenode. net"); bot.joinChannel("#pircbot"); } catch (NickAlreadyInUseException e) { e. printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch ( IrcException e) { e.printStackTrace(); } } public void stop(BundleContext context) throws Exception { bot.disconnect(); bot.dispose(); } }
All this does is create an IRCBot instance, with a ServiceLookup implementation. Now for the IRCBot itself, in all its primitive glory:
package ircbot; import org.jibble.pircbot.PircBot; import service.ServiceLookup; import repository.RepositoryService; import repository.Node; public class IRCBot extends PircBot { ServiceLookup service; public IRCBot(ServiceLookup service) { super(); this.service=service; setName("OSGIBot"); } @Override protected void onMessage(String channel, String sender, String login, String hostname, String message) { String[] command=message.split(" "); if(command.length>1 && ("~set".equals(command[0]) || "~get".equals(command[0]))) { String path=command[ 1]; // we should use a tracker for this! RepositoryService repository= ( RepositoryService) service.getService(RepositoryService.class.getName()); if("~ set".equals(command[0])) { StringBuilder content=new StringBuilder(); for(int i= 2;i<command.length;i++) { content.append(" "); content.append(command[i]); } Node node=repository.get(path); if(node==null) { node=new Node(); node.setAuthor (sender); node.setSource("irc"); } node.getContents().add(content.toString(). trim()); repository.put(path, node); } if("~get".equals(command[0])) { Node node =repository.get(path); if(node!=null) { int count=0; // will only do two at most, to be polite for(String content:node.getContents()) { if(count++>2) { break; } sendMessage(channel, sender + ": "+content); } } } } } }
And lastly, our MANIFEST.MF, which specifies our classpath (which has to include pircbot.jar), and the activator name ("ircbot.BotActivator"), and the external dependency on " repository":
Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: IRCBot Plug-in Bundle-SymbolicName: ircbot Bundle-Version: 1.0.0 Bundle-Activator: ircbot. BotActivator Bundle-Vendor: theserverside.com Import-Package: org.osgi.framework; version="1.3.0" Require-Bundle: repository Bundle-ClassPath: pircbot.jar,.
Installing this bundle and starting it will (noisily) connect to Freenode and join #pircbot. Note that there's no code in there to handle nick collisions; you may want to try to add that yourself, or change the default nickname used. It's also not very good code; anyone can put in anything, and let's just say it wouldn't be too hard to crash in multiple ways. However...
There's nothing here that says that only the IRCBot can use the repository. Theoretically, a Jabber client could use the same repository (and very nearly the same code for managing the commands.) In fact, this sort of thing is where OSGi shines: the code in the IRCBot to handle commands could itself be contained in a bundle, and the IRCBot would simply let the appropriate bundle manage commands as needed, and they'd look up the repository service whenever the repository was required.
Conclusion
Hopefully, this has illuminated readers to some of the potential of OSGi, as well as providing a start on how to use it.
Read Peter Kriens article on why OSGi trumps Spring IoC for de-coupling and control