OSGi-ifying JAudioTagger
15 Aug 2012
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:
- Work out the packages that should be exposed in JAudioTagger so that my code can use it effectively
- Work out the packages that JAudioTagger itself requires
- 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.