Tuesday 20 August 2013

Jigsaw launcher for Felix

This post will show a simple launcher module for Felix. This launcher takes advantage of the exported packages list that can be obtained from Jigsaw's modules to fill in the list of JRE's packages, thus eliminating the need to hard code that list in Felix defaults.

Setting up the environement:

  1. Build Jigsaw
  2. Build the Maven plugin for Jigsaw 
  3. Download and extract Maven 3(.0.5) in ~/dev
  4. Export the variables:
    export M2_HOME=~/dev/apache-maven-3.0.5/
    export MAVEN_HOME=~/dev/apache-maven-3.0.5/
    export JAVA_HOME=~/dev/jigsaw/build/linux-x86_64-normal-server-release/images/jdk-module-image
    export PATH=$JAVA_HOME/bin:$M2_HOME/bin:$PATH

Create the launcher
  1.  Create the directories
    mkdir -p ~/dev/felix/felix-launcher/src/main/java/lh.jigsaw.felix.launcher/lh/jigsaw/felix/launcher
    mkdir ~/dev/felix/bundles
    mkdir ~/dev/felix/nonbundles
    mkdir ~/dev/felix/modules
  2. Create the modules library
    jmod create -L ~/dev/felix/modules
  3. Download Felix
    wget -O ~/dev/felix/nonbundles/org.apache.felix.framework-4.2.1.jar http://mirror.tcpdiag.net/apache//felix/org.apache.felix.framework-4.2.1.jar
    (although Felix framework is a bundle it is used as a non bundle)
  4. Add the pom
    gedit ~/dev/felix/felix-launcher/pom.xml &
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <groupId>lh.jigsaw</groupId>
        <artifactId>jigsaw-felix-launcher</artifactId>
        <version>0.1-SNAPSHOT</version>
        <name>Jigsaw Felix launcher</name>
        <packaging>jmod</packaging>
      <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
      </properties>
        <build>     
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>2.3.2</version>
                </plugin>
                <plugin>
                    <groupId>lh.jigsaw</groupId>
                    <artifactId>jigsaw-maven-plugin</artifactId>
                    <version>0.1-SNAPSHOT</version>
                    <extensions>true</extensions>
                    <configuration>
                      <libraryDirectory>/home/ludovic/dev/felix/modules</libraryDirectory>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </project>
    change the libraryDirectory to your appropriate path
  5. Add the module-info
    gedit ~/dev/felix/felix-launcher/src/main/java/lh.jigsaw.felix.launcher/module-info.java &
    module lh.jigsaw.felix.launcher @ 0.1
    {
      requires jdk.jre;
      class lh.jigsaw.felix.launcher.Main;
    }
    (depends on jdk.jre to expose the JRE)
  6. Add the Main class
    gedit ~/dev/felix/felix-launcher/src/main/java/lh.jigsaw.felix.launcher/lh/jigsaw/felix/launcher/Main.java &
    package lh.jigsaw.felix.launcher;

    import java.io.File;
    import java.io.IOException;
    import java.lang.module.ModuleId;
    import java.lang.module.ModuleInfo;
    import java.lang.module.ModuleView;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    import java.net.MalformedURLException;
    import java.net.URL;
    import java.net.URLClassLoader;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.ServiceLoader;
    import org.openjdk.jigsaw.SimpleLibrary;

    /**
     *
     * @author ludovic
     */
    public class Main
    {
      public static void main(final String[] args)
      {
        try
        {    
          // Extract information from Jigsaw
          //
         
          final String libraryPath = System.getProperty("sun.java.launcher.module.library");
         
          final SimpleLibrary library = SimpleLibrary.open(new File(libraryPath));
         
          final StringBuilder packages = new StringBuilder();
          final List<URL> urls = new ArrayList<>(); // 'classpath' for launching Felix
         
          for (ModuleId id:  library.listModuleIds())
          {
            if (isJreModule(id.name()))
            {
              urls.add(library.classPath(id).toURI().toURL());
             
              final ModuleInfo info = library.readModuleInfo(id);
              appendExportedPackages(info, packages);
            }
          }
         
          packages.deleteCharAt(packages.length()-1);
         
          final File userDir = new File(System.getProperty("user.dir"));
         
          // Additional "classpath" (Felix's jar is added there)
          //     
          addNonOsgiJars(userDir, urls);
         
          // Initialise and start OSGi
          //     
          startOsgi(userDir, urls, packages.toString());
               
        }
        catch (IOException | ClassNotFoundException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException | IllegalAccessException ex)
        {
          throw new RuntimeException("error initialising", ex);
        }
       
        System.out.println("started.");
       
      }

      private static boolean isJreModule(final String moduleName)
      {
        return moduleName.startsWith("java") || moduleName.startsWith("javax");
      }
     
      private static void appendExportedPackages(final ModuleInfo info, final StringBuilder packages)
      {
        final ModuleView view = info.defaultView();
        for (String exportedPackage: view.exports())
        {
          if (!isJavaPackage(exportedPackage))
          {
            packages.append(exportedPackage).append(';');
          }
        }
      } 
     
      private static boolean isJavaPackage(final String packageName)
      {
        // java.* are always available to bundles
        return packageName.startsWith("java.");
      }
     
      private static void addNonOsgiJars(final File userDir, final List<URL> urls) throws MalformedURLException
      {   
        final File bundlesDir = new File(userDir, "nonbundles");
        for (File file : bundlesDir.listFiles())
        {
          urls.add(file.toURI().toURL());
        }
      }
     
      // As Jigsaw currently identificate itself as Java 1.8, and as Felix already has Java 1.8 package,
      // override the full system packages, including Felix's own
      private static final String FELIX_SYSTEM_PACKAGES =
        "org.osgi.framework; version=1.7.0," +
        " org.osgi.framework.hooks.bundle; version=1.1.0," +
        " org.osgi.framework.hooks.resolver; version=1.0.0," +
        " org.osgi.framework.hooks.service; version=1.1.0," +
        " org.osgi.framework.hooks.weaving; version=1.0.0," +
        " org.osgi.framework.launch; version=1.1.0," +
        " org.osgi.framework.namespace; version=1.0.0," +
        " org.osgi.framework.startlevel; version=1.0.0," +
        " org.osgi.framework.wiring; version=1.1.0," +
        " org.osgi.resource; version=1.0.0," +
        " org.osgi.service.packageadmin; version=1.2.0," +
        " org.osgi.service.startlevel; version=1.1.0," +
        " org.osgi.service.url; version=1.0.0," +
        " org.osgi.util.tracker; version=1.5.1 ";
     
      private static Map<String, Object> getFrameworkProperties(final String packages)
      {
        final Map config = new HashMap<>();
        config.put("org.osgi.framework.system.packages", FELIX_SYSTEM_PACKAGES + "," + packages);
    //    config.put("org.osgi.framework.system.packages.extra", packages);
        return config;
      }
     
     
      // we use reflection to not depend on OSGi core (and to not make a module of it)
     
      private static final Class<?>[] NO_ARG_SIG = null;
      private static final Object[] NO_ARG_INV = null;
     
      private static void startOsgi(final File userDir, final List<URL> urls, final String packages) throws ClassNotFoundException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException, IllegalAccessException
      {     
          final URLClassLoader cl = getClassLoaderFor(urls);
         
          final Class<?> bundleClass = cl.loadClass("org.osgi.framework.Bundle");     
          final Class<?> bundleContextClass = cl.loadClass("org.osgi.framework.BundleContext");     
          final Class<?> frameworkFactoryClass = cl.loadClass("org.osgi.framework.launch.FrameworkFactory");
         
          final Map<String, Object> properties = getFrameworkProperties(packages);
         
          final Object framework = getFrameworkFor(frameworkFactoryClass, cl, properties);     
          final Class<?> frameworkClass = framework.getClass();
         
          initialiseFramework(frameworkClass, framework);
         
          final Object bundleContext =  getBundleContextFor(frameworkClass, framework);
         
          final List<Object> installedBundles = new ArrayList<>();     
          installBundles(installedBundles, userDir, bundleContextClass, bundleContext);      
          startBundles(installedBundles, bundleClass);
         
          startFramework(frameworkClass, framework);
         
      }

      private static URLClassLoader getClassLoaderFor(final List<URL> urls)
      {
        final URL[] urlsArray = urls.toArray(new URL[urls.size()]);
       
        return new URLClassLoader(urlsArray);
      }

      private static Object getFrameworkFor(final Class<?> frameworkFactoryClass, final URLClassLoader cl, final Map<String, Object> properties) throws IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException, IllegalAccessException
      {
        final ServiceLoader<?> sl = ServiceLoader.load(frameworkFactoryClass, cl);
        final Object factory = sl.iterator().next();
       
        final Method newFramework = factory.getClass().getMethod("newFramework", Map.class);
        return newFramework.invoke(factory, properties);
      }

      private static Object getBundleContextFor(final Class<?> frameworkClass, final Object framework) throws NoSuchMethodException, IllegalAccessException, SecurityException, IllegalArgumentException, InvocationTargetException
      {
        final Method getBundleContext = frameworkClass.getDeclaredMethod("getBundleContext", NO_ARG_SIG);
        final Object bundleContext =  getBundleContext.invoke(framework, NO_ARG_INV);
        return bundleContext;
      }

      private static void initialiseFramework(final Class<?> frameworkClass, final Object framework) throws InvocationTargetException, SecurityException, NoSuchMethodException, IllegalAccessException, IllegalArgumentException
      {
        final Method init = frameworkClass.getDeclaredMethod("init", NO_ARG_SIG);
        init.invoke(framework, NO_ARG_INV);
      }
     
      private static void installBundles(final List<Object> installedBundles, final File userDir, final Class<?> bundleContextClass, final Object bundleContext) throws NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException
      {   
        final Method installBundle = bundleContextClass.getDeclaredMethod("installBundle", String.class);
       
        final File bundlesDir = new File(userDir, "bundles");
        for (File file : bundlesDir.listFiles())
        {
          final String name = file.toURI().toString();
          final Object bundle = installBundle.invoke(bundleContext, name);
          installedBundles.add(bundle);
        }
      }

      private static void startBundles(final List<Object> installedBundles, final Class<?> bundleClass) throws NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException
      {  
        final Method start = bundleClass.getDeclaredMethod("start", NO_ARG_SIG);
       
        for (Object bundle: installedBundles)
        {
          start.invoke(bundle, NO_ARG_INV);
        }
      }

      private static void startFramework(final Class<?> frameworkClass, final Object framework) throws IllegalAccessException, SecurityException, NoSuchMethodException, InvocationTargetException, IllegalArgumentException
      {
        final Method start = frameworkClass.getDeclaredMethod("start",  NO_ARG_SIG);
        start.invoke(framework, NO_ARG_INV);
      }
    }
  7. Make
    cd ~/dev/felix/felix-launcher/
    mvn install
     
 Test with a sample bundle
  1. Create the directories
    mkdir -p ~/dev/felix/simplebundle/src/main/java/lh/jigsaw/test/simplebundle
  2.  Add the pom
    gedit ~/dev/felix/simplebundle/pom.xml &
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>

        <groupId>lh.jigsaw.test</groupId>
        <artifactId>simplebundle</artifactId>
        <version>1.0-SNAPSHOT</version>
        <packaging>bundle</packaging>

        <name>simplebundle OSGi Bundle</name>

        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>

        <dependencies>
            <dependency>
                <groupId>org.osgi</groupId>
                <artifactId>org.osgi.core</artifactId>
                <version>4.3.0</version>
                <scope>provided</scope>
            </dependency>
        </dependencies>

        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.felix</groupId>
                    <artifactId>maven-bundle-plugin</artifactId>
                    <version>2.3.7</version>
                    <extensions>true</extensions>
                    <configuration>
                        <instructions>
                            <Bundle-Activator>lh.jigsaw.test.simplebundle.Activator</Bundle-Activator>
                            <Export-Package/>
                        </instructions>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </project>
  3. Add the bundle activator
    gedit ~/dev/felix/simplebundle/src/main/java/lh/jigsaw/test/simplebundle/Activator.java &
    package lh.jigsaw.test.simplebundle;

    import javax.swing.JOptionPane;
    import org.osgi.framework.BundleActivator;
    import org.osgi.framework.BundleContext;

    public class Activator implements BundleActivator
    {
      public void start(BundleContext context) throws Exception
      {
        System.out.println("Hello Jigsaw/Felix!");
        JOptionPane.showMessageDialog(null, "Hello Jigsaw/Felix!");
      }

      public void stop(BundleContext context) throws Exception
      {
      }
    }
  4. Make
    (as the bundle is compiled with a compiler that does not know about Jigsaw, we must use the standard Java 7)
    export JAVA_HOME='/usr/lib/jvm/java-7-openjdk-amd64'
    cd ~/dev/felix/simplebundle/
    mvn install

  5. Copy to the bundle directory
    cp ~/dev/felix/simplebundle/target/simplebundle-1.0-SNAPSHOT.jar ~/dev/felix/bundles
 Run
cd ~/dev/felix/
java -L ~/dev/felix/modules -m lh.jigsaw.felix.launcher