package com.macrobug.osgiWrapper;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;
import org.eclipse.core.runtime.adaptor.EclipseStarter;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
/**
* Class which will take the name of a 'runnable' and run it with access to certain OSGi APIs.
* The contents of this file are hereby released into the public domain.
* Note that this code is fragile and rubbish and exists only as a proof-of-concept.
* @author adriantaylor
*
*/
public class OsgiWrapper {
/**
* Run the given 'Runnable' in a class loader which has access to certain APIs defined by OSGi/Eclipse
* plug-ins.
* @param classNameWeMustLoad The name of a Runnable. You can get this using <tt>MyRunnable.class.getName()</tt>.
* This Runnable will be instantiated programmatically, so it must be public and have a public zero-argument
* constructor. The code within this plug-in will be executed using a {@link ClassLoader} which has access
* to the APIs in the OSGi plug-in, as well as your own code.
* <p>
* Currently, this Runnable must be found on the class path and not inside a JAR file. It won't work if you
* have the Runnable in a JAR file or if you're using some wacky {@link ClassLoader} yourself.
* @param installDirectory The directory in which an installation of Eclipse can be found which contains all
* the required plug-ins.
* @param packages A list of packages which you wish to import from Eclipse plug-ins. Currently only the first is made available.
* @throws Exception All kinds of things can go wrong.
*/
@SuppressWarnings("unchecked")
public static void runInOsgi(String runnableClassName,
String installDirectory, String... packages) throws Exception {
/* Set up OSGI. */
System.setProperty("eclipse.application",
"com.macrobug.osgiWrapper.plugin.DynamicApplication");
System.setProperty("osgi.configuration.area", installDirectory
+ "/configuration");
System.setProperty("osgi.install.area", "file://" + installDirectory);
System.setProperty("osgi.framework", "file://" + installDirectory
+ "/plugins/" + findOsgiPluginName(new File(installDirectory + "/plugins/")));
BundleContext ctx = EclipseStarter.startup(new String[] {}, null);
/* Fabricate a fake Eclipse plug-in which asks for access to the desired packages.
* We actually construct the binary JAR content for this plug-in at run-time. This
* dynamic plug-in contains an IApplication which we will run when we start Eclipse.
* This will simply pass the ClassLoader back to us so we can use it. */
Bundle b = ctx.installBundle("com.macrobug.osgiWrapper.plugin", createDynamicPlugin(packages));
b.start();
/* Start Eclipse and run the IApplication. This will return its ClassLoader. */
ClassLoader osgiInnerClassLoader = (ClassLoader) EclipseStarter.run(null);
/* Now create a hybrid class loader which will search for classes in the OSGi plug-ins
* before falling back to our existing class loader. */
ClassLoader l = new OsgiMungedClassLoader(
osgiInnerClassLoader, OsgiWrapper.class.getClassLoader(),runnableClassName);
/* Install this as our context class loader, in case anybody actually cares. */
Thread.currentThread().setContextClassLoader(l);
/* Finally, ask this new class loader to load our Runnable... */
Class<? extends Runnable> c = (Class<? extends Runnable>) l.loadClass(runnableClassName);
/* ... and run it. */
Runnable r = c.newInstance();
/* Anything that this Runnable does will now be done in the context of a class loader
* which is able to load and use the APIs from the OSGi plug-in. */
r.run();
}
/**
* Find the OSGi core plug-in so that we can set OSGi properties properly.
* @param directory The plug-in directory
* @return The name of the OSGi plug-in
* @throws FileNotFoundException If it didn't seem to exist
*/
private static String findOsgiPluginName(File directory) throws FileNotFoundException {
for (String s : directory.list()) {
if (s.startsWith("org.eclipse.osgi_"))
return s;
}
throw new FileNotFoundException("Unable to find org.eclipse.osgi plugin");
}
/**
* Return an InputStream which contains our dynamic plug-in.
* @param packages The packages which our plug-in should import.
* @return An InputStream from which the plug-in can be read
* @throws IOException If something goes wrong
*/
private static InputStream createDynamicPlugin(String[] packages) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
writePlugin(packages, baos);
byte[] pluginBytes = baos.toByteArray();
return new ByteArrayInputStream(pluginBytes,0,pluginBytes.length);
}
/**
* Write our dynamic plug-in to an OutputStream. This can be used to write it to a file
* for test purposes.
* @param packages The packages which our plug-in should import
* @param baos The output stream
* @throws IOException In case of problem
*/
private static void writePlugin(String[] packages,
OutputStream baos) throws IOException, Error {
JarOutputStream jos = new JarOutputStream(baos);
try {
putDirEntry("META-INF/",jos);
ZipEntry mf = new ZipEntry("META-INF/MANIFEST.MF");
jos.putNextEntry(mf);
Writer w = new OutputStreamWriter(jos);
w.write("Manifest-Version: 1.0\n");
w.write("Bundle-ManifestVersion: 2\n");
w.write("Bundle-Name: Plugin Plug-in\n");
w.write("Bundle-SymbolicName: com.macrobug.osgiWrapper.plugin;singleton:=true\n");
w.write("Bundle-Version: 1.0.0\n");
w.write("Bundle-Vendor: MACROBUG\n");
w.write("Bundle-RequiredExecutionEnvironment: J2SE-1.5\n");
w.write("Require-Bundle: org.eclipse.core.runtime;bundle-version=\"3.4.0\"\n");
w.write("Import-Package: "+packages[0]+"\n");
w.flush();
jos.closeEntry();
ZipEntry xml = new ZipEntry("plugin.xml");
jos.putNextEntry(xml);
w = new OutputStreamWriter(jos);
w.write(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<?eclipse version=\"3.2\"?>\n"+
"<plugin>\n"+
" <extension\n"+
" id=\"DynamicApplication\"\n"+
" name=\"DynamicApplication\"\n"+
" point=\"org.eclipse.core.runtime.applications\">\n"+
" <application\n"+
" cardinality=\"singleton-global\"\n"+
" thread=\"main\"\n"+
" visible=\"true\">\n"+
" <run\n"+
" class=\"com.macrobug.osgiwrapper.plugin.DynamicApplication\">\n"+
" </run>\n"+
" </application>\n"+
" </extension>\n"+
"</plugin>\n"
);
w.flush();
jos.closeEntry();
putDirEntry("com/",jos);
putDirEntry("com/macrobug/",jos);
putDirEntry("com/macrobug/osgiwrapper/",jos);
putDirEntry("com/macrobug/osgiwrapper/plugin/",jos);
ZipEntry clz = new ZipEntry("com/macrobug/osgiwrapper/plugin/DynamicApplication.class");
jos.putNextEntry(clz);
/* The following line refers to a resource which is the byte code for this:
* package com.macrobug.osgiwrapper.plugin;
* import org.eclipse.equinox.app.IApplication;
* import org.eclipse.equinox.app.IApplicationContext;
* public class DynamicApplication implements IApplication {
* public Object start(IApplicationContext context) throws Exception {
* return getClass().getClassLoader();
* }
* public void stop() {
* }
* }
*/
InputStream is = OsgiWrapper.class.getResourceAsStream("/DynamicApplication.bin");
if (is == null)
throw new IOException("Unable to getResourceAsStream");
int oneByte;
while ((oneByte = is.read()) != -1)
jos.write(oneByte);
jos.closeEntry();
} finally {
jos.close();
}
}
/**
* Small utility method to create a directory in a JAR.
*/
private static void putDirEntry(String string, JarOutputStream jos) throws IOException {
ZipEntry direntry = new ZipEntry(string);
jos.putNextEntry(direntry);
}
/**
* Class loader which is able to pull classes from either inside the world of OSGi
* plug-ins or outside.
* It also has special explicit handling for a certain class, passed in as "classNameWeMustLoad".
* This will always be defined by this class, even if it's readily available from other class
* loaders. This appears to be the only way to force Java to ask this class loader when it wants
* to load other classes. We must actually be the <em>defining</em> class loader for the
* Runnable; merely being the <em>initiating</em> class loader is insufficient.
* @author adriantaylor
*/
private static class OsgiMungedClassLoader extends ClassLoader {
private ClassLoader osgiClassLoader;
private ClassLoader realWorldClassLoader;
private String classNameWeMustLoad;
private Collection<File> dirs;
/**
* Constructor.
* @param osgiInnerClassLoader The class loader which has been forcibly extracted from OSGi.
* This can be used to load APIs from the OSGi plug-ins.
* @param classLoader The class loader from the real, 'outside' world. This can be used
* to load other classes which have nothing to do with OSGi.
* @param classNameWeMustLoad The name of a class which we must explicitly and definitely
* load, even if it's available elsewhere.
*/
public OsgiMungedClassLoader(ClassLoader osgiInnerClassLoader,
ClassLoader classLoader, String classNameWeMustLoad) {
this.dirs = makeDirs();
this.osgiClassLoader = osgiInnerClassLoader;
this.realWorldClassLoader = classLoader;
this.classNameWeMustLoad = classNameWeMustLoad;
}
private static Collection<File> makeDirs() {
ArrayList<File> urls = new ArrayList<File>();
StringBuffer current = new StringBuffer();
String all = System.getProperty("java.class.path");
for (int i=0;i<all.length();i++) {
if (all.charAt(i) == File.pathSeparatorChar) {
urls.add(new File(current.toString()));
current = new StringBuffer();
} else {
current.append(all.charAt(i));
}
}
urls.add(new File(current.toString()));
return urls;
}
/*
* (non-Javadoc)
* @see java.lang.ClassLoader#loadClass(java.lang.String)
* Load a class. If we have been asked to load the Runnable, we always
* load it, even if it's available from one of the other class loaders.
* This is a hack but we have no choice - we simply must load it ourselves
* in order to be recorded as its defining class loader. Then, any classes
* it loads will be requested via us - and we can pass them onto the OSGi
* class loader.
*/
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (classNameWeMustLoad.equals(name)) {
String binname = name.replaceAll("\\.", "/");
binname = binname + ".class";
for (File f : dirs) {
try {
if (f.isDirectory()) {
File classfile = new File(f,binname);
if (classfile.canRead()) {
byte[] bytes = new byte[(int)classfile.length()+2];
FileInputStream fis = new FileInputStream(classfile);
try {
int numRead = fis.read(bytes);
return defineClass(name, bytes, 0, numRead);
} finally {
fis.close();
}
}
}
} catch (IOException e) {}
}
throw new Error("Unable to find class "+name+" by searching for "+binname+" within "+dirs.toString());
}
/* It's not the runnable - so see whether the class is available from the OSGi
* class loader (which has access to the Eclipse/OSGi APIs in which we're interested)
* - if not, go back to the 'outside' class loader. */
try {
Class<?> c = osgiClassLoader.loadClass(name);
if (c != null)
return c;
} catch (ClassNotFoundException e) {}
return realWorldClassLoader.loadClass(name);
}
}
}