Monday, April 28, 2008

Creating OSGi bundles from any JAR

One of the major pains in OSGi is the creation of bundles. Peter Kriens' BND tool does a great job - it's powerful and can be used for creating the most complex OSGi bundles.

But haven't you ever wished for a simple tool at which you just throw an existing JAR file and it produces the corresponding OSGi bundle jar? Well, I have - and I have written the class below which does exactly that. And, yes, I have been found guilty of reinventing the wheel (see comment below) - a simple wrapper to BND would have achieved the same purpose except I learnt a few things along the way doing it myself :-)

This is a convenient way to quickly produce an OSGi bundle to be deployed in your OSGi runtime.

Clearly, this is not a replacement for BND or similar tools. It is a good 80% solution. Hopefully, we will soon see reasonably searchable OSGi bundle repositories come up (and I do not mean what is currently offered on the OSGi alliance site - you can't even search for bundles offering a certain class or package, to name just one use case). Hopefully, we will also see bundles offered for all of the major open source packages, not just a chosen few used in Eclipse. Ok, rant finished.

Here comes the code. [Deleted my comment about reinventing the weel from the original post - admittedly I have been found guilty].

And before I forget, this code depends on the following JARs:

  • Dependency: I had to adapt it to the current version of ASM (that was simple, the source code is available). If you send me an email, I can send you my version of the code.
  • ASM
  • Apache Commons IO

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.vafer.dependency.utils.DependencyUtils;
import org.apache.commons.io.FileUtils;

/**
* This class implements a simplistic approach to creating
* bundles from any JAR file.
*
* @author Mathias Richter
*/
public class Osgify
{

/**
* Return all packages imported by classes in the specified jar file.
*
* @param jarFileName The name of the JAR file to analyse.
* @return A set of package names.
*
* @throws java.io.FileNotFoundException
* @throws java.io.IOException
*/
public static Set<String> getImportPackages( String jarFileName ) throws FileNotFoundException, IOException
{
HashMap<String, String> packages = new HashMap<String, String>();
Set<String> dependencies = DependencyUtils.getDependenciesOfJar( new FileInputStream( jarFileName ) );
for ( String name : dependencies )
{
if ( !name.startsWith( "[" ) )
{
int index = name.lastIndexOf( "." );
if ( index > 0 )
name = name.substring( 0, index );
} else
{
if ( name.length() > 2 )
{
int index = name.lastIndexOf( "." );
if ( index > 2 )
name = name.substring( 2, index );
} else
continue;
}
if ( !name.startsWith( "java." ) )
packages.put( name, name );
}
return packages.keySet();
}

/**
* Return a set of names of the packages declared in the specified JAR file.
*
* @param jarFileName The name of the JAR file to analyse.
* @return The names of the packages declared by the classes in the JAR file.
*
* @throws java.io.IOException
*/
public static Set<String> getExportPackages( String jarFileName ) throws IOException
{
HashMap<String, String> packages = new HashMap<String, String>();
JarFile file = new JarFile( jarFileName );
for ( Enumeration<JarEntry> e = file.entries(); e.hasMoreElements();)
{
String name = e.nextElement().getName();
if ( name.endsWith( ".class" ) )
{
name = name.replaceAll( "/", "." );
int index = name.lastIndexOf( "." );
if ( index > 0 )
name = name.substring( 0, name.lastIndexOf( "." ) );
index = name.lastIndexOf( "." );
if ( index > 0 )
name = name.substring( 0, name.lastIndexOf( "." ) );
packages.put( name, name );
}
}
return packages.keySet();
}

/**
* Analyse the specified JAR for its package imports and package declarations and
* create a manifest file describing these in OSGi fashion.
*
* @param jarFileName The name of the JAR file for which to create an OSGi bundle manifest. May not be null.
* @param bundleName The name of the bundle to delcrae in the manifest. May not be null.
* @param bundleSymbolicName The symbolic name to declare in the manifest. If null, uses the bundle name.
* @param bundleVersion The version to declare in the manifest. If null, uses "1.0.0".
* @param bundleActivator The activator class to declare in the manifest. If null, does not specify an activator.
* @param resolveOptional If true, all package imports will be marked with "resolution:=optional".
*
* @return The manifest contents as a string.
*
* @throws java.io.FileNotFoundException
* @throws java.io.IOException
*/
public static String getManifest( String jarFileName, String bundleName, String bundleSymbolicName, String bundleVersion, String bundleActivator, boolean resolveOptional ) throws FileNotFoundException, IOException
{
StringBuffer manifest = new StringBuffer();
Set<String> im = getImportPackages( jarFileName );
Set<String> ex = getExportPackages( jarFileName );
im.removeAll( ex );
manifest.append( "Bundle-Name: " );
manifest.append( bundleName );
manifest.append( "\n" );
if ( bundleSymbolicName == null )
bundleSymbolicName = bundleName;
manifest.append( "Bundle-SymbolicName: " );
manifest.append( bundleSymbolicName );
manifest.append( "\n" );
if ( bundleVersion == null )
bundleVersion = "1.0.0";
manifest.append( "Bundle-Version: " );
manifest.append( bundleVersion );
manifest.append( "\n" );
if ( bundleActivator != null )
{
manifest.append( "Bundle-Activator: " );
manifest.append( bundleActivator );
manifest.append( "\n" );
}
manifest.append( "Import-Package: " );
boolean previous = false;
for ( String p : im )
{
if ( previous )
manifest.append( "," );
manifest.append( p );
if ( resolveOptional )
manifest.append( ";resolution:=optional" );
previous = true;
}
manifest.append( "\n" );
manifest.append( "Export-Package: " );
previous = false;
for ( String p : ex )
{
if ( previous )
manifest.append( "," );
manifest.append( p );
previous = true;
}
manifest.append( "\n" );
manifest.append( "\n" );
return split( manifest.toString(), 70 );
}

/**
* Format the specified source string to use a max line length as specified.
* Lines will be broken in "manifest" fashion, ie. continuation lines start with
* a single space.
*
* @param source the source String to format
* @param maxLineLength The maximum line length in the result.
* @return The formatted string.
*/
protected static String split( String source, int maxLineLength )
{
StringBuffer result = new StringBuffer();
StringTokenizer tokens = new StringTokenizer( source, "\n" );
while ( tokens.hasMoreTokens() )
{
String line = tokens.nextToken();
int length = line.length();
for ( int index = 0; index < length;)
{
if ( index > 0 )
result.append( " " );
int remainder = length - index;
if ( remainder <= maxLineLength )
{
result.append( line.substring( index, length ) );
index = length;
} else
{
result.append( line.substring( index, index + maxLineLength ) );
index += maxLineLength;
}
result.append( "\n" );
}
}
return result.toString();
}

/**
* If the process has output, return it as a String.
*
* @param process The process.
* @return The process output as String.
* @throws java.io.IOException
*/
protected static String getOutput( Process process ) throws IOException
{
StringBuffer result = new StringBuffer();
InputStream is = process.getInputStream();
InputStreamReader isr = new InputStreamReader( is );
BufferedReader br = new BufferedReader( isr );
String line = null;
while ( ( line = br.readLine() ) != null )
result.append( line );
is.close();
isr.close();
br.close();
return result.toString();
}

/**
* Create an OSGi jar from the specified JAR file using the specified bundle name.
* The OSGi bundle jar will be created in the same directory as the original JAR.
*
* @param jarFileName The JAR file to analyse and produce an OSGi bundle from.
* @param bundleName the name of the bundle.
*
* @throws java.io.FileNotFoundException
* @throws java.io.IOException
* @throws java.lang.InterruptedException
*/
public static void osgify( String jarFileName, String bundleName ) throws FileNotFoundException, IOException, InterruptedException
{
osgify( jarFileName, bundleName, null, null, null, true );
}

/**
* Create an OSGi jar from the specified JAR file using the specified bundle name and bundle symbolic name.
* The OSGi bundle jar will be created in the same directory as the original JAR.
*
* @param jarFileName The JAR file to analyse and produce an OSGi bundle from.
* @param bundleName the name of the bundle.
* @param bundleSymbolicName The symbolic bundle name to use.
*
* @throws java.io.FileNotFoundException
* @throws java.io.IOException
* @throws java.lang.InterruptedException
*/
public static void osgify( String jarFileName, String bundleName, String bundleSymbolicName ) throws FileNotFoundException, IOException, InterruptedException
{
osgify( jarFileName, bundleName, bundleSymbolicName, null, null, true );
}

/**
* Create an OSGi jar from the specified JAR file using the specified bundle name and bundle symbolic name.
* The OSGi bundle jar will be created in the same directory as the original JAR. *
*
* @param jarFileName The name of the JAR file for which to create an OSGi bundle manifest. May not be null.
* @param bundleName The name of the bundle to declare in the manifest. May not be null.
* @param bundleSymbolicName The symbolic name to declare in the manifest. If null, uses the bundle name.
* @param bundleVersion The version to declare in the manifest. If null, uses "1.0.0".
* @param bundleActivator The activator class to declare in the manifest. If null, does not specify an activator.
* @param resolveOptional If true, all package imports will be marked with "resolution:=optional".
*
* @throws java.io.FileNotFoundException
* @throws java.io.IOException
* @throws java.lang.InterruptedException
*/
public static void osgify( String jarFileName, String bundleName, String bundleSymbolicName, String bundleVersion, String bundleActivator, boolean resolveOptional ) throws FileNotFoundException, IOException, InterruptedException
{
int index = jarFileName.lastIndexOf( File.separator );
String targetDir = "./";
if ( index > 0 )
targetDir = jarFileName.substring( 0, index + 1 );
File tempDir = new File( targetDir + ".osgify" + System.currentTimeMillis() );
FileUtils.forceMkdir( tempDir );
try
{
Process p = Runtime.getRuntime().exec( "jar xf " + jarFileName, null, tempDir );
if ( p.waitFor() != 0 )
throw new Error( "Could not expand '" + jarFileName + "' in temporary directory: " + getOutput( p ) );
File manifest = new File( tempDir.getAbsolutePath() + "/META-INF/MANIFEST.MF" );
if ( manifest.exists() )
FileUtils.forceDelete( manifest );
manifest = new File( tempDir.getAbsolutePath() + "/META-INF/MANIFEST.MF" );
String manifestContent = getManifest( jarFileName, bundleName, bundleSymbolicName, bundleVersion, bundleActivator, resolveOptional );
FileUtils.writeStringToFile( manifest, manifestContent );
String netJarName = jarFileName.substring( jarFileName.lastIndexOf( File.separator ) + 1, jarFileName.lastIndexOf( "." ) );
String targetJarName = targetDir + netJarName + "_" + bundleVersion + ".jar";
String cmd = "jar cfm " + targetJarName + " META-INF/MANIFEST.MF ./*";
p = Runtime.getRuntime().exec( cmd, null, tempDir );
if ( p.waitFor() != 0 )
throw new Error( "Could not create OSGi bundle jar '" + targetJarName + "' in temporary directory: " + getOutput( p ) );
} finally
{
FileUtils.forceDelete( tempDir );
}
}

}



1 comment:

Anonymous said...

Seems like you could have created a simple wrapper command for BND, no?