Wednesday, August 12, 2009

Groovy + OSGi: Gradle Makes it Easy

I'm exploring the tooling around Groovy and OSGi for the September Groovy.MN meeting, where I'm presenting Groovy+OSGi Jumpstart (and then on to 2GX!).

In the end, creating OSGi bundles from Groovy classes was pretty darn easy with Gradle. The User Guide documentation wasn't exactly clear, and the current 0.7 sample project didn't do much of anything... so here's the goods while you wait for the updates to released.

A good background on using Groovy and OSGi is the Beginner's Guide to OSGi on the Desktop. It explains bundles and Activators are, and what the Import-Package and Export-Package metadata specifies. In a nutshell: an OSGi bundle is a JAR file with extra data in the MANIFEST.MF. It's dependencies are specified in the Import-Package attribute and it's exported API is specified in the Export-Package attribute. On bundle startup, a method called Activator.start() is called, and on shutdown Activator.shutdown() is called. The bundle replaces the JAR as a unit of deployment, the dependency management metadata replaces JAR Hell, and the Activator replaces "public static void main".

This post will show you a few things:

  • how to implement an Activator in Groovy
  • how valid bundle metadata looks
  • how to create the bundle from Gradle
  • how to load and execute the bundle in Eclipse Equinox
A Groovy Activator
This couldn't be much simpler. Implement the org.osgi.framework.BundleActivator interface in Groovy. You now have knowledge of 4% of the OSGi API (it's only 27 classes, you know).
package org.gradle

import org.osgi.framework.BundleActivator
import org.osgi.framework.BundleContext

public class GradleActivator implements BundleActivator {

public void start(BundleContext context) {
println "Hello from a Groovy Gradle Activator"
}

public void stop(BundleContext context) {
}
}
Notice, I put this in the org.gradle package (it's important later). Now think for a moment about all the public methods on this object... can you name all 16 methods Groovy adds?.

Valid Bundle Metadata
The bundle metadata in the JAR's MANIFEST.MF file needs to specify, at a minimum, a unique SymbolicName, all the ImportPackages that the class will need to run, and all the ExportPackages that other classes will need to run it.

What does this activator need for ImportPackage? It needs to import org.gradle (itself), and it needs to import org.osgi.framework (the interface). You also need to import groovy.lang and a few other Groovy packages so that Groovy method dispatch works correctly.

What does the activator need for ExportPackage? Certainly org.gradle, so that the container can invoke the Activator. What else? This hidden dependencies are in the silent API of Groovy objects. All Groovy objects have a get/setMetaClass that operates on type groovy.lang.MetaClass. So to use the Activator you need to export groovy.lang and a few other packages, even if you don't invoke those methods!

A Gradle Generated MANIFEST.MF

Luckily, the Gradle OSGi plugin handles all of the ImportPackage and ExportPackage dependencies for you. Here is the full MANIFEST that Gradle generates:
Manifest-Version: 1.0
Ant-Version: Apache Ant 1.7.0
Created-By: 1.6.0_04 (Sun Microsystems Inc.)
Export-Package: org.gradle;uses:="groovy.lang,org.codehaus.groovy.refl
ection,org.codehaus.groovy.runtime,org.osgi.framework,org.codehaus.gr
oovy.runtime.callsite";version="1.0"
Bundle-Version: 1.0
Tool: Bnd-0.0.255
Bnd-LastModified: 1250128067203
Bundle-Name: Example Gradle Activator
Bundle-ManifestVersion: 2
Bundle-Activator: org.gradle.GradleActivator
Import-Package: groovy.lang;version="1.6",org.codehaus.groovy.reflecti
on;version="1.6",org.codehaus.groovy.runtime;version="1.6",org.codeha
us.groovy.runtime.callsite;version="1.6",org.gradle;version="1.0",org
.osgi.framework;version="1.4"
Bundle-SymbolicName: gradle_tooling.osgi
It's a mouthful! Gotta love the wrapping rules on manifest files. Did you know the wrapping is defined not on the number of characters in a line but the number of bytes? Ugh.

Anway, as you can see, the Export-Package and Import-Package are correct. Groovy was discovered and specified. You can also see in the Tool attribute that bnd is used under the covers.

In Gradle, you use the OSGi plugin to simplify the generation of the manifest. The following build file specifies the Equinox dependency (to resolve the OSGi classes) and the Groovy dependency (to compile the Groovy). The "configure(jar.osgi)" begins the OSGi configuration. By specifying the * for import and export packages, bnd is invoked to determine the dependencies. 22 lines of build script gives you a "gradle jar" target that creates a valid bundle:
group = 'gradle_tooling'
version = '1.0'

usePlugin 'groovy'
usePlugin 'osgi'

repositories {
mavenRepo(urls: 'http://repository.jboss.org/maven2/')
}

dependencies {
groovy group: 'org.codehaus.groovy', name: 'groovy-all', version: '1.6.0'
compile( 'org.eclipse:osgi:3.4.3.R34x_v20081215-1030' )
}

configure(jar.osgi) {
version = '1.0'
name = 'Example Gradle Activator'
instruction 'Bundle-Activator', 'org.gradle.GradleActivator'
instruction 'Import-Package', '*'
instruction 'Export-Package', '*'
}

Proof Positive
It's a hello world bundle, right? So let's run it!

Invoke the OSGi container in console mode:
java -jar %osgi_jar% -console
The Gradle script will stick the OSGi jar in the .gradle cache. Mine is at $home/.gradle/cache/org.eclipse/osgi/jars/osgi-3.4.3.R34x_v20081215-1030.jar

Now just install and start the bundle:
osgi> ss

Framework is launched.

id State Bundle
0 ACTIVE org.eclipse.osgi_3.4.0.v20080605-1900

osgi> install file:/d:/libs/groovy-all-1.6-beta-2.jar
Bundle id is 27

osgi> install file:/d:/libs/osgi-1.0.jar
Bundle id is 28

osgi> ss

Framework is launched.

id State Bundle
0 ACTIVE org.eclipse.osgi_3.4.0.v20080605-1900
27 INSTALLED groovy-all_1.6.0.beta-2
28 INSTALLED gradle_tooling.osgi_1.0.0

osgi> start 28
Hello from a Groovy Gradle Activator

osgi>

Wheee! It works; hooray for Gradle, Groovy and OSGi... a trifecta of un-suck.

6 comments:

Wolfgang Schell said...

Hi Hamlet,

AFAIK, you don't have to export the package of the Activator, if it is not to be used from other bundles. The OSGi framework starts the Activator without it being exported.

Regards,

Wolfgang

Gregory Boissinot's Blog said...

Good post!

Some feedback:

Currently, JBoss Maven repository doesn't contain the groovy artifact with version 1.6.0. So, you have to use a more Maven repository like the Maven central repository.

Change the repositories section :

with groovy 1.6.0
repositories {
mavenCentral()
mavenRepo(urls: 'http://repository.jboss.org/maven2/')
}

or with the latest groovy version (v1.6.4)
repositories {
mavenCentral()
mavenRepo(urls: 'http://repository.jboss.org/maven2/')
}

dependencies {
groovy group: 'org.codehaus.groovy', name: 'groovy-all', version: '1.6.4'
compile( 'org.eclipse:osgi:3.4.3.R34x_v20081215-1030' )
}

Hamlet D'Arcy said...

Thanks for the feedback!

@Gregory - OK, the 1.6 jar must have been cached for me then b/c it worked on my machine :). I'll change it.

@Wolfgang - Cool, I could have sworn it was failing without this export. I'll remove it and see if it still works.

Robert Fischer said...

Inspired by your post, I started heading this way.

When I try to run, things are great until it tries to fire off my Activator. It goes through instantiating the Activator, which hits the constructor of my class, which then goes off into static-metaClass-building land, and finally explodes in the reflection inside java.security.AccessController.doPrivileged.

Here's the exception:

Exception in thread "main" org.osgi.framework.BundleException: Activator start error in bundle groovy16 [3].
Caused by: java.lang.NoClassDefFoundError: org/w3c/dom/NodeList

Did you ever see this?

Hamlet D'Arcy said...

@Robert Yes, this error message is obvious! :)

You have not started a bundle that exports the org.w3c.dom package or
your bundle does not import org.w3c.dom.

You need to find a Jar that has the org.w3c.dom package in it's export list in MANIFEST.MF. Then start that bundle before your bundle. Or just add org.w3c.dom to your import list.

Hamlet D'Arcy said...

@Robert

Final solution looks to be:

You must need to tell your container to expose that package as part of the system bundle.

Something like this in config.ini:

org.osgi.framework.system.packages=org.w3c.dom,org.w3c.dom.traversal, org.w3c.dom.ls, javax.sql, javax.transaction

Check out these directions:

http://docs.codehaus.org/display/JETTY/OSGi+Tips