OSGi-ifying JAudioTagger

15 Aug 2012 Cockpit Poser

Whilst porting bliss to the OSGi platform, to enable in-app updates, I realised I'd have to port some of the third party libraries bliss used too. One of these, JAudioTagger, is a popular library used by many so I thought it might be useful to describe my steps porting the vanilla JAudioTagger JAR to an OSGi bundle.

During May and June this year I took on the task of adding in-app updates to bliss. The approach I took was to convert bliss to run on the OSGi platform. OSGi is a platform upon which you develop highly modularised software components. One of the side benefits of OSGi is that, because code dependencies and versions are explicitly defined, the framework is able to swap different versions in and out at runtime. This means it can be used as a way of implementing in-app update.

That sounds great, but it took an awful lot more effort than I was expecting. Converting bliss to OSGi meant converting each part of bliss into an OSGi bundle. A bundle is basically a module; a set of cohesive Java classes where both the classes' imported dependencies and exported code are explicitly defined.

In practicality, what is a bundle? Most commonly it's a standard JAR file containing Java classes and other resources but with a MANIFEST.MF file with special headers to communicate the imports and exports I mentioned. MANIFEST.MF files are nothing new; they are a standard part of JAR files. They are stored in a META-INF folder in the root of the JAR. Here's an edited-for-brevity MANIFEST.MF for one of the bliss bundles:

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: com.elsten.util
Bundle-SymbolicName: com.elsten.util
Bundle-Version: 1.1.6.0
Export-Package: com.elsten.apache.commons.httpclient;version="1.0.0",
 com.elsten.collections;version="1.0.0",
 com.elsten.concurrent;version="1.0.0",
 com.elsten.lang;version="1.0.0",
 com.elsten.util;version="1.2.0",
 com.elsten.xml;version="1.0.0"
Bundle-Vendor: elsten software limited
Import-Package: com.google.common.base;version="11.0.1",
 com.google.common.collect;version="11.0.1",
 javax.xml.namespace,
 javax.xml.parsers,
 javax.xml.transform,
 javax.xml.transform.dom,
 javax.xml.transform.stream,
 javax.xml.xpath,
 org.apache.commons.httpclient;version="3.1.0",
 org.apache.commons.io;version="1.4.0",
 org.apache.commons.lang;version="2.6.0",
 org.apache.log4j;version="1.2.16",
 org.w3c.dom,
 org.xml.sax

The Manifest-Version header has been around since MANIFEST.MF files were born, but the other headers are all OSGi-specific. Most speak for themselves but, pertinently, imports are defined by Import-Package and exports by Export-Package. You can see that imports and exports are specified at the package level. At runtime, if a class inside the com.elsten.util bundle attempts to load third party code in a package not included in Import-Package, a ClassNotFoundException results. Similarly, external bundles attempting to use classes inside com.elsten.util will only be permitted to use those defined explicitly under Export-Package.

So you can see there's a fair bit of work deciding what packages should be exported and imported. Fortunately there are tools to do this for us, such as bnd which is capable of auto generating the MANIFEST.MF file.

But it wasn't just the bliss code that had to be OSGi-ified (as I've come to term it). All of bliss's dependencies, the third party libraries it uses, also needed to be converted for use in OSGi. In some cases, for the more popular libraries such as the Apache Commons libraries and Google Guava there were already OSGi enabled bundles. However, for other dependencies there were no such pre-created libraries.

One of the most crucial third party libraries in bliss's architecture is JAudioTagger. JAudioTagger is a library that can open up music files and read or write their metadata and artwork. It supports multiple music file formats ( although not, sadly, AIFF) so it's ideal as a one-stop shop for music tagger software.

Making JAudioTagger into an OSGi bundle follows a similar pattern to OSGi-ifying other libraries:

  1. Work out the packages that should be exposed in JAudioTagger so that my code can use it effectively
  2. Work out the packages that JAudioTagger itself requires
  3. Write the MANIFEST.MF file for the JAudioTagger bundle JAR

Initially I created the MANIFEST.MF file myself manually, but laterly I discovered bndtools, an Eclipse plugin based on bnd which I mentioned before. Using the Wrap JAR as OSGi Bundle Project I was able to supply the JAudioTagger JAR and have bnd automatically work out the dependencies. The imports were worked out as:

Import-Package: javax.imageio,
	javax.imageio.stream,
	javax.swing.filechooser,
	sun.misc,
	sun.nio.ch,
	sun.security.action

Working out which packages to export, however, still takes work. So I decided to be lazy and just export the lot. Here's what I added to the bnd.bnd file central to bndtools development:

Export-Package: org.jaudiotagger;version=2.0.5,
	org.jaudiotagger.audio;version=2.0.5,
	org.jaudiotagger.audio.asf;version=2.0.5,
	org.jaudiotagger.audio.asf.data;version=2.0.5,
	org.jaudiotagger.audio.asf.io;version=2.0.5,
	org.jaudiotagger.audio.asf.util;version=2.0.5,
	org.jaudiotagger.audio.exceptions;version=2.0.5,
	org.jaudiotagger.audio.flac;version=2.0.5,
	org.jaudiotagger.audio.flac.metadatablock;version=2.0.5,
	org.jaudiotagger.audio.generic;version=2.0.5,
	org.jaudiotagger.audio.mp3;version=2.0.5,
	org.jaudiotagger.audio.mp4;version=2.0.5,
	org.jaudiotagger.audio.mp4.atom;version=2.0.5,
	org.jaudiotagger.audio.ogg;version=2.0.5,
	org.jaudiotagger.audio.ogg.util;version=2.0.5,
	org.jaudiotagger.audio.real;version=2.0.5,
	org.jaudiotagger.audio.wav;version=2.0.5,
	org.jaudiotagger.audio.wav.util;version=2.0.5,
	org.jaudiotagger.logging;version=2.0.5,
	org.jaudiotagger.tag;version=2.0.5,
	org.jaudiotagger.tag.asf;version=2.0.5,
	org.jaudiotagger.tag.datatype;version=2.0.5,
	org.jaudiotagger.tag.flac;version=2.0.5,
	org.jaudiotagger.tag.id3;version=2.0.5,
	org.jaudiotagger.tag.id3.framebody;version=2.0.5,
	org.jaudiotagger.tag.id3.reference;version=2.0.5,
	org.jaudiotagger.tag.id3.valuepair;version=2.0.5,
	org.jaudiotagger.tag.images;version=2.0.5,
	org.jaudiotagger.tag.lyrics3;version=2.0.5,
	org.jaudiotagger.tag.mp4;version=2.0.5,
	org.jaudiotagger.tag.mp4.atom;version=2.0.5,
	org.jaudiotagger.tag.mp4.field;version=2.0.5,
	org.jaudiotagger.tag.reference;version=2.0.5,
	org.jaudiotagger.tag.vorbiscomment;version=2.0.5,
	org.jaudiotagger.tag.vorbiscomment.util;version=2.0.5,
	org.jaudiotagger.test;version=2.0.5,
	org.jaudiotagger.utils;version=2.0.5,
	org.jaudiotagger.utils.tree;version=2.0.5

Although I used bndtools to generate the bundle and the MANIFEST.MF contained within, I am still using Eclipse PDE at coding time. At this point, with the JAudioTagger bundle deployed to my PDE target platform, my code was able to correctly Import-Package the packages exported by the JAudioTagger MANIFEST.MF. All my code compiled nicely and I could run a full build to generate the bliss application. When bliss is run, it uses an alternative OSGi container, Apache Felix. I deployed to Felix and... it broke. The JAudioTagger bundle began throwing ClassNotFoundExceptions when attempting to load some of the classes in the packages it had imported, such as sun.nio.ch.directbuffer.

Even though I had imported the sun.nio.ch package the OSGi container was not able to 'wire' my import to a corresponding export. Experienced Java developers will know that, in the Sun and OpenJDK Java distributions at least, sun.nio.ch is shipped inside the Java libraries. In vanilla Java applications, sun.nio.ch is available because when the Java classpath is formed the entire JAR becomes available.

But OSGi is different. The OSGi specification mandates that a few packages are always available to bundles such as the java.* packages containing all the contents of the standard core Java API. However, sun.nio.ch is not one of these packages. Therefore, even though the package is there in our Java runtime, we cannot access it. We have to find out a way of exporting this package.

There are a number of options for exporting core Java API packages but I settled on the system bundle fragment approach. Using this approach, a an OSGi fragment is created that exports the required packages. An OSGi fragment is a bundle that attaches to a host bundle and provides extra or replacement configuration and code. In this example, our fragment exports extra packages, including sun.nio.ch. Here's what our fragment's MANIFEST.MF (omiting some other declarations) looks like:

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: com.elsten.bliss.osgi.system
Bundle-SymbolicName: com.elsten.bliss.osgi.system
Bundle-Version: 1.0.1.0
Bundle-Vendor: elsten software limited
Fragment-Host: system.bundle; extension:=framework
Bundle-RequiredExecutionEnvironment: JavaSE-1.6
Export-Package: sun.nio.ch,
 sun.security.action,
 sun.misc,
 javax.swing

The fragment attaches to the system bundle with the declaration Fragment-Host and uses Export-Package to export the necessary packages for JAudioTagger.

I've uploaded the JAudioTagger OSGi MANIFEST here. To convert your 'normal' JAudioTagger JAR into a bundle you must update your existing JAR's META-INF/MANIFEST.MF file with this replacement, or with your own hand rolled replacement. Remember you'll have to recreate this each time you upgrade JAudioTagger.

Thanks to Presleyjesus for the image above.
blog comments powered by Disqus