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);
    }
  }
}