Gradle Equinox Plugin – Part 4: How to install bundles providing console commands

updated_logo_for_gradle

In the previous part, I described how to implement the creation of an empty Equinox, but this is not very functional yet. So in this post I’ll write down how to code the part which installs bundles in the container.

    Equinox is a framework using units which are divided into two types:

  1. Bundles – a set of Java class files with much more detailed MANIFEST.MF file. The manifest describes the behavior of the bundle in the framework, e.g. what is needed from the outside world to fulfill its dependencies, the packages that should be exported, what must be executed when the bundle is being started or stopped, etc.
    MANIFEST.MF example file:
    Bundle-Name: Hello OSGi World
    Bundle-SymbolicName: name.mazgalov.helloworld
    Bundle-Description: A Hello World bundle
    Bundle-ManifestVersion: 2
    Bundle-Version: 1.0.0
    Bundle-Activator: name.mazgalov.helloworld.Activator
    Export-Package: name.mazgalov.helloworld;version="1.0.0"
    Import-Package: org.osgi.framework
    

    The lines from the example are called OSGi headers and describe the behavior of the bundle in the environment. These are not all of them, a list can be found here: OSGi bundles headers reference

    The headers above are the most used ones. They serve for:
    Bundle-Name: Defines a human-readable name for the bundle.
    Bundle-SymbolicName: The only mandatory header. It specifies a unique identifier for a bundle, based on the reverse domain name convention (similar to the java packages).
    Bundle-Description: A description of the functionality of the bundle.
    Bundle-ManifestVersion: The OSGi specification which must be used for reading the bundle.
    Bundle-Version: The version number of the bundle.
    Bundle-Activator: Indicates the class name to be invoked when the bundle is activated.
    Export-Package: A list with Java packages contained in the bundle which will be made available to the outside world.
    Import-Package: A list with Java packages which are required from the outside world to fulfill the dependencies needed in a bundle.

  2. Fragment – Java archive file with specific manifest headers that enable it to attach to a specified host bundle. Fragments are treated as part of the host, including any permitted headers. The fragment uses the host classloader and it must never have own. To specify the fragment host, must be used the Fragment-Host manifest header. Typically it is used to provide files for different locales and to provide some platform specific code.

The Equinox framework supports specification of the bundles start order – start levels. First bundles which will be started are these with start level 1, second – with level 2, etc.

To operate over the framework, a console will be needed, so this will be first bundles for installation. I prefer the Apache Felix framework console bundles. They are a set of jars: org.apache.felix.gogo.shell, org.apache.felix.gogo.runtime, org.apache.felix.gogo.command. All of them are required to enable the console.

To enable installation of bundles in the plugin, following changes are required:

...
void apply(Project project) {
        this.project = project

        ConfigurationContainer configurations = project.configurations
        DependencyHandler dependencies = project.dependencies
        TaskContainer tasks = project.tasks

        project.repositories {
            mavenCentral()
        }

        configurations.create 'equinoxKernel'
        configurations.create 'bundlesLoader', {
            transitive = false
        }
        dependencies.add 'equinoxKernel', [group:'org.eclipse', name: 'osgi', version: '3.10.0-v20140606-1445', configuration: 'runtime']
        dependencies.add 'bundlesLoader', [group:'org.apache.felix', name: 'org.apache.felix.gogo.shell', version: '0.12.0']
        dependencies.add 'bundlesLoader', [group:'org.apache.felix', name: 'org.apache.felix.gogo.runtime', version: '0.12.0']
        dependencies.add 'bundlesLoader', [group:'org.apache.felix', name: 'org.apache.felix.gogo.command', version: '0.12.0']

        tasks.create 'buildEquinoxConfig', {
            doLast {
                def equinoxBuildDir = new File("$project.buildDir/equinox")
                Path equinoxKernelFile =
                        configurations.equinoxKernel.incoming.files.singleFile.toPath() // Equinox Kernel file

                def bundlesList = configurations.bundlesLoader.findResults { jar ->
                    if(!jar.name.contains('sources')) { // Exclude sources
                        jar.withInputStream { stream ->
                            JarInputStream jarInputStream = null
                            try {
                                jarInputStream = new JarInputStream(stream)
                                java.util.jar.Manifest mf = jarInputStream.getManifest();
                                if(mf != null) {
                                    if(mf.mainAttributes.containsKey(new java.util.jar.Attributes.Name('Fragment-Host'))) {
                                        "reference:file:$jar.path@1"
                                    } else {
                                        "reference:file:$jar.path@1:start"
                                    }
                                }
                            } finally {
                                if(jarInputStream != null) {
                                    jarInputStream.close()
                                }
                                if(stream != null) {
                                    stream.close()
                                }
                            }
                        }
                    }
                }

                Properties configurationProperties = new Properties()
                configurationProperties['osgi.install.area'] = equinoxBuildDir.toPath().toAbsolutePath().toUri().toString()
                configurationProperties['osgi.framework'] = "file:${equinoxBuildDir.toPath().relativize(equinoxKernelFile)}".toString()
                configurationProperties['osgi.noShutdown'] = 'true'
                configurationProperties['eclipse.consoleLog'] = 'true'
                configurationProperties['eclipse.ignoreApp'] = 'true'
                configurationProperties['osgi.bundles'] = bundlesList.join(',')

                def equinoxConfigurationDir = new File("$equinoxBuildDir/configuration")
                equinoxConfigurationDir.mkdirs()
                def equinoxConfigurationFile = new File(equinoxConfigurationDir, 'config.ini')
                equinoxConfigurationFile.withOutputStream {
                    configurationProperties.store it, null/*No comments*/
                }
            }
        }
...

First will be needed a configuration which will store all bundles for installation providing basic functionality (like console commands):

        
configurations.create 'bundlesLoader', {
    transitive = false
}

The configuration transitive dependencies are disabled, because I want the get only the first level dependencies. For example, if the Felix bundles depends of another OSGi framework, it will not be delivered.

After that, the required dependencies must be specified:

dependencies.add 'bundlesLoader', [group:'org.apache.felix', name: 'org.apache.felix.gogo.shell', version: '0.12.0']
dependencies.add 'bundlesLoader', [group:'org.apache.felix', name: 'org.apache.felix.gogo.runtime', version: '0.12.0']
dependencies.add 'bundlesLoader', [group:'org.apache.felix', name: 'org.apache.felix.gogo.command', version: '0.12.0']

At the current state, Equinox doesn’t know which units must be installed. To set, an additional property is required named osgi.bundles:

configurationProperties['osgi.bundles'] = bundlesList.join(',')

The osgi.bundles property represents a comma-separated list of bundles which are automatically installed and optionally started once the system is up and running. Each entry is of the form:

<URL | simple bundle location>[@ [<start-level>] [“:start”]]

The start-level indicates the OSGi start level at which the bundle should run. If the “start” tag is added then the bundle will be marked as started after being installed. Simple bundle locations are interpreted as relative to the framework’s parent directory. The bundles that we want to install are not located under the framework’s parent directory, hence must be used the reference protocol.

bundlesList is an object representing a list of strings defining the options: URL location of the bundle, start level and auto start flag:

def bundlesList = configurations.bundlesLoader.findResults { jar ->
    if(!jar.name.contains('sources')) { // Exclude sources. Sources jars are not needed in the container.
        jar.withInputStream { stream ->
            JarInputStream jarInputStream = null
            try {
                jarInputStream = new JarInputStream(stream)
                java.util.jar.Manifest mf = jarInputStream.getManifest();
                if(mf != null) {
                    if(mf.mainAttributes.containsKey(new java.util.jar.Attributes.Name('Fragment-Host'))) {
                        "reference:file:$jar.path@1"
                    } else {
                        "reference:file:$jar.path@1:start"
                    }
                }
            } finally {
                if(jarInputStream != null) {
                    jarInputStream.close()
                }
                if(stream != null) {
                    stream.close()
                }
            }
        }
    }
}

All incoming jars manifest files will be scanned to recognize the bundles and fragments. All bundle files will be auto started after the installation due to the start flag which is being set for them.

Now let’s execute the test to generate an Equinox container:

>gradlew clean test -i

When the generated equinox.bat file is started, the output is:
equinox-bat-console-gogo

Trying some commands in the console:
equinox-bat-console-gogo-2

As a conclusion, en Equinox framework with enabled console command can be created by the plugin. As next actions the tasks defined in the plugin’s apply method can be removed and rewritten as separate ones extending the Gradle DeafultTask API. This will allow a configuration by the script which applies the plugin, e.g. additional bundles for installation, creation of multiple containers, etc.

I hope that this was useful for you 🙂

elephant-th

3 thoughts on “Gradle Equinox Plugin – Part 4: How to install bundles providing console commands

    • admin says:

      Hi fortran,

      At the current state, custom bundles cannot be installed. To solve this, I’m preparing the next stage of the tutorial. The code is ready, if you are interested, you can find it on github – https://github.com/t-mazgalov/gradle-equinox/tree/part-5 (branch part-5). Another option is to wait for the next article with the code explanations. I hope it will be available soon.

      Regards,
      Todor

Leave a Reply

Your email address will not be published. Required fields are marked *