20 August 2015

Switching To OSGi R6 And Dumping bnd Annotations

This article describes how a product consisting of multiple bnd projects using the bnd annotations was switched to an OSGi R6 runtime and converted to OSGi R6 annotations.

Updates

  • 20150825
    bndtools will process Declarative Services annotations and metatype annotations by default in its latest development (3.0.0.DEV) build (see the next update note).
  • 20150821
    bndtools will process Declarative Services annotations and metatype annotations by default in a future development (3.0.0.DEV) build, which means that - from that point on - the '-dsannotations: *' and '-metatypeannotations: *' instructions will no longer be needed.
    See bndtools Github issue 1041: OSGi annotation processing default.
  • 20150821
    Added a note about '-dsannotations-options: inherit' in section 6.1
  • 20151007
    Updated section 6.2.2: dynamic services need 'policy = ReferencePolicy.DYNAMIC'.


0. Prerequisites


The conversion process described in this article was performed on on a Fedora 22 x86_64 Linux machine running an Eclipse Mars installation on top of OpenJDK 8.

The product that was converted is contained in a bndtools workspace.

In order to be able to develop for - and use - the OSGi R6 annotations and runtime the latest development build of bndtools (3.0.0.DEV) is needed. The easiest way to obtain it is to install the latest development build of the Pelagic bndtools plugins since that will also automatically install the latest development build of bndtools.

The Pelagic bndtools plugins provide a Gradle build environment that is much more flexible than the one that bndtools itself provides, and also has more features, like support for Findbugs and Jacoco (code coverage).

Make sure that the Gradle templates (containg bnd bundles) in the cnf project have been updated to the latest status. This will ensure that the offline build will works the same as the Eclipse build.

In order to prevent doing many changes and losing them because of a small mistake it is advised to make heavy use of a version control system and committing often.


1. Referenced bnd Annotations


The bnd annotations that are used by the product are of multiple types.

Tooling Annotations

  • @ConsumerType
  • @ProviderType

Declarative Services Annotations

  • @Activate
  • @Component
  • @Deactivate
  • @Modified
  • @Reference

Metatype Annotations

  • @Meta.AD
  • @Meta.OCD


2. Converting The Runtime


The first step in the conversion process is an easy step and consist of converting the runtime to OSGi R6.

2.1. OSGi Runtime Bundles


The converted product runs within in an Apache Felix runtime, so update all Felix bundles to their latest released versions.

Download all relevant bundles from their download page and put the downloaded bundles in the bndtools 'Local' bundles repository.

2.2. OSGi Core And Compendium Bundles


The converted product uses both the OSGi Core and OSGi Compendium bundles in the build, so update these to OSGi R6 bundles as well.

Download the bundles from their download page and put the downloaded bundles in the bndtools 'Build' bundles repository.

2.3. OSGi Annotation Bundle


The OSGi R6 equivalent of the bnd tooling annotations are contained in the OSGi R6 annotation bundle.

Download the bundle from their download page and put the downloaded bundle in the bndtools 'Build' bundles repository.

2.4. Switching The Runtime To OSGi R6


The product uses definitions in the main bnd configuration file (cnf/build.bnd) for the OSGi bundles.
osgi.core.version: 6.0.0
osgi.cmpn.version: 6.0.0
These definitions are then used in bnd bundle descriptors (<project>/bnd.bnd) to put the correct OSGi bundles on the build path:
-buildpath: \
    osgi.core;version=${osgi.core.version},\
    osgi.cmpn;version=${osgi.cmpn.version},\
    ...

2.5. Wrap-up


Compile, test and run the product; verifying that everything still works is a Very Good Idea.

For example: the converted product implemented an OSGi core interface class in a test-support class, which needed adjustments because that interface gained new methods in OSGi R6.

When everything checks out then commit the changes into version control.


3. Converting Tooling Annotations


This step involves replacing the '@ProviderType' and '@ConsumerType' bnd annotations with the ones from OSGi R6.

3.1. bnd Bundle Descriptors


Add a definition for the OSGi R6 annotation bundle in the main bnd configuration file (cnf/build.bnd):
osgi.annotation.version: 6.0.1
This definition can then be used in bnd bundle descriptors (<project>/bnd.bnd) to put the OSGi annotation bundles on the build path:
-buildpath: \
    osgi.core;version=${osgi.core.version},\
    osgi.cmpn;version=${osgi.cmpn.version},\
    osgi.annotation;version=${osgi.annotation.version},\
    ...

3.2. Finding Projects To Convert


In Eclipse, open the 'Search' dialog by typing Ctrl+H, go to the 'File Search' tab and search for the following regular expression in every file in the workspace:
(@(Provider|Consumer)Type|biz.aQute.bnd.annotation)
This will list every file in the workspace where adjustments are needed. Generally these files will be API classes and bnd bundle descriptors.

3.3. Converting Tooling Annotations


The actual conversion is quite simple and generally consists of two steps:

1. Replace the bnd annotation bundle on the build path with the OSGi R6 annotation jar. 
 -buildpath: \
     ...

-    biz.aQute.bnd.annotation,\
+    osgi.annotation;version=${osgi.annotation.version},\
     ...

2. Fix the imports of the annotation types.
-import aQute.bnd.annotation.ProviderType;
-import aQute.bnd.annotation.ConsumerType;
+import org.osgi.annotation.versioning.ProviderType;
+import org.osgi.annotation.versioning.ConsumerType;

Note that the bnd annotation bundle can't be removed from projects using other bnd annotations such as '@Activate', '@Modified', etc.

However, the converted product separates API and implementation by requiring separate projects for API and implementation, which makes this concern void. This specific conversion therefore becomes quite mechanical.

3.4. Wrap-up


Compile, test and run the product; verifying that everything still works is a Very Good Idea.

When everything checks out then commit the changes into version control.



4. Preparing Metatype Conversion


Before the actual metatype conversion can be performed some adjustments have to be made to the configuration interface classes.

4.1. Configuration Interface Classes


The product uses the ConfigAdmin service to provision its bundles with configuration.

bnd metatype support is employed in the build to generate the correct metadata in the bundle, and it is used during runtime to convert a map of injected configuration properties (in '@Activate' and '@Modified' methods) into an instance of the corresponding configuration interface class of the bundle.

An example of such an interface class:
package nl.pelagic.arp;

import aQute.bnd.annotation.metatype.Meta;

@Meta.OCD
public interface Config {
  static final String DEFAULT = "5000";
  static final String MIN     = "1000";

  @Meta.AD(min = MIN, deflt = DEFAULT, required = false)
  int queryIntervalMilliSeconds();
}

And the code that is used to convert a map of injected configuration properties into an instance of the configuration interface class:
@Activate
void activate(final Map<String, Object> props) {
  ...
  config = Configurable.createConfigurable(Config.class, props);
  ...
}

OSGi R6 uses annotation classes for injected configuration properties instead of interface classes as done by bnd. Annotation classes only allow basic types, so all configuration interface classes must be converted to only use basic types. Also, defaults for configuration properties are defined by defaults of the annotation class methods in OSGi R6, which means that any defaults should be of the same type as the return type of configuration interface method to which it applies.

For the example shown above this results the following conversion:
-static final String DEFAULT = "5000";
-static final String MIN     = "1000";
+static final int DEFAULT = 5000;
+static final int MIN     = 1000;

-@Meta.AD(min = MIN, deflt = DEFAULT,
+@Meta.AD(min = "" + MIN, deflt = "" + DEFAULT
, required = false)

Which results in the following interface class:
package nl.pelagic.arp;

import aQute.bnd.annotation.metatype.Meta;

@Meta.OCD
public interface Config {
  static final int DEFAULT = 5000;
  static final int MIN     = 1000;

  @Meta.AD(min = "" + MIN, deflt = "" + DEFAULT, required = false)
  int queryIntervalMilliSeconds();
}

Also, methods like (also for other types than String)
List<String> networkInterfaces();
must be converted to use basic type like
String[] networkInterfaces();

Note that the conversion of List<String> into String[] might require code changes in classes that invoke the corresponding method(s).

Also, be aware that comparing two List<String> objects is quite valid while comparing two String[] objects is not!

4.2. Annotation Proxy


Many tests for the product test the dynamic behaviour of the bundles and inject maps with configuration properties into '@Activate' and '@Modified' methods.

OSGi R6 injects the configuration properties through instances of configuration annotation classes.

In order to be able to easily construct such instances, an annotation proxy is added to the test support classes.

Download the classes 'com.googlecode.miyamoto.AnnotationProperty' and 'com.googlecode.miyamoto.AnnotationProxyBuilder' from the Google miyamoto project and place them somewhere conveniently. In the converted product the downloaded classes were placed in a test support project.


4.3. Wrap-up


Compile, test and run the product; verifying that everything still works is a Very Good Idea.

When everything checks out then commit the changes into version control.


5. Metatype Conversion


5.1. bnd Bundle Descriptors


In order to use the OSGi R6 metatype annotations in a project, its bnd bundle descriptor (bnd.bnd) must be adjusted. This is accomplished by adding the following instruction:
-metatypeannotations: *
This instruction tells bnd to expect OSGi R6 metatype annotations.

5.2. Metatype Annotations


OSGi R6 uses annotation classes for injected configuration properties instead of interface classes as done by bnd.

Also, defaults for configuration properties are defined by defaults of the annotated methods.

5.2.1. Configuration Interface Classes


The above means that the configuration interface classes must be converted into annotation classes and their use of bnd metatype annotations must be replaced by use of OSGi R6 metatype annotations.

Some attributes of the OSGi R6 metatype annotations are named differently or work differently than those of the bnd metatype annotations:
  • @Meta.OCD --> @ObjectClassDefinition
    Simple rename, works the same for the bundles of the converted product.
  • @Meta.AD --> @AttributeDefinition
    Simple rename with some renamed attributes, works mostly the same for the bundles of the converted product.
    Attribute 'deflt' was renamed to 'defaultValue'.
Note that this list is not exhaustive, it's just what was encountered in the conversion of the product.

Also note that the 'defaultValue' attribute is dropped from the annotation in the conversion and instead the default is assigned to the annotated method.

For the example used earlier:
-import aQute.bnd.annotation.metatype.Meta;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;

-@Meta.OCD
+@ObjectClassDefinition
 public interface Config {
   static final int MIN     = 1000;
   static final int DEFAULT = 5000;

-  @Meta.AD(min = "" + MIN, deflt = "" + DEFAULT, required = false)

+  @AttributeDefinition(min = "" + MIN, required = false)
-  int queryIntervalMilliSeconds();
+  int queryIntervalMilliSeconds() default DEFAULT;

 }

Next, the interface class must be converted into an annotation class.

For the example used earlier:
-public interface Config {
+@interface Config {

5.2.2. Implementation Classes


With bnd, the metatype designate of a component is declared in the '@Component' annotation. However, in OSGi R6 there is a dedicated annotation for that.

For the example used earlier:
+import org.osgi.service.metatype.annotations.Designate;

-@Component(designate = Config.class, provide = {})
+@Component(provide = {})
+@Designate(ocd = Config.class)

OSGi R6 injects instances of the configuration annotations directly into a component. The bnd helper is no longer needed and the method signatures can be adjusted to take such instances directly.

For the example used earlier:
 @Activate
-void activate(final Map<String, Object> props) {
+void activate(final Config config) {
   ...
-  config = Configurable.createConfigurable(Config.class, props);
   ...

 @Modified
-void modified(final Map<String, Object> props) {
+void modified(final Config config) {
   ...
-  config = Configurable.createConfigurable(Config.class, props);
   ...

5.2.3. Test Classes


Since configuration is now injected as instances of configuration annotation classes, the test classes must be adjusted to do just that.

Creating an instance of a configuration annotation class:
+import com.googlecode.miyamoto.AnnotationProxyBuilder;

-void fillProps(final Map<String, Object> props) {
+void fillProps(final AnnotationProxyBuilder<Config> config) {
-  props.put("queryIntervalMilliSeconds", Config.DEFAULT);
+  config.setProperty("queryIntervalMilliSeconds", Config.DEFAULT);
}

-final Map<String, Object> props = new HashMap<>();
+final AnnotationProxyBuilder<Config> config =
+        AnnotationProxyBuilder.newBuilder(Config.class);
-this.fillProps(props);
+this.fillProps(config);

Invoking '@Activate' and '@Modified' methods:
-this.impl.activate(props);
+this.impl.activate(config.getProxedAnnotation());

-this.impl.modified(props);
+this.impl.modified(config.getProxedAnnotation());

5.3. Wrap-up


The product can't be compiled, tested and run just yet because the metatype and Declarative Services conversion has to be done at the same time (at least in the way the conversion is described).

Just to be safe after having done so much work, temporarily commit the changes into version control.


6. Declarative Services Conversion


6.1. bnd Bundle Descriptors


In order to use the OSGi R6 Declarative Services annotations in a project, its bnd bundle descriptor (bnd.bnd) must be adjusted.

For the example used earlier:
-Service-Component: *
+-dsannotations: *

 -buildpath:  \
     osgi.core;version=${osgi.core.version},\
     osgi.cmpn;version=${osgi.cmpn.version},\
     osgi.annotation;version=${osgi.annotation.version},\
-    biz.aQute.bnd.annotation,\
     ...

- Conditional-Package: \
-    aQute.bnd.annotation.metatype

Three changes are made in the bnd bundle descriptor:
  1. The 'Service-Component' instruction that tells bnd to expect bnd Declarative Services annotations is replaced by the '-dsannotations' instruction that tells bnd to expect OSGi R6 Declarative Services annotations.
  2. The bnd annotations bundle is removed from the build path. The OSGi R6 annotations are contained in the OSGi R6 compendium bundle (the metatype annotations, which are also contained in that bundle, were already converted)
  3. The bnd metatype annotation package is removed from the 'Conditional-Package' (static linking) instruction.
      The reason that this package was statically linked is that the implementation uses the 'Configurable.createConfigurable()' helper, which is located in that package. Using the helper creates a dependency on bndlib, which is quite large. The size of bndlib is too large for the converted product so only the needed package (which is quite small) was included in the bundle(s).

Note that the following instruction is needed when a component extends a (abstract) class with OSGi annotations, like @Activate, etc:
-dsannotations-options: inherit

6.2. Declarative Services Annotations


Letting Eclipse organise the imports will fix the imports of the Declarative Services annotations.

For the example used earlier:
-import aQute.bnd.annotation.component.Activate;
-import aQute.bnd.annotation.component.Component;
-import aQute.bnd.annotation.component.Deactivate;
-import aQute.bnd.annotation.component.Modified;
-import aQute.bnd.annotation.component.Reference;
-import aQute.bnd.annotation.metatype.Configurable;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.component.annotations.Reference;
This will generally fix most of the compilation errors, specifically those of the '@Activate', '@Deactivate' and '@Modified' annotations. However, the '@Component' and '@Reference' annotations will still show compilation errors.

6.2.1. @Component


Some attributes of the OSGi R6 '@Component' annotation are named differently or work differently than those of the bnd '@Component' annotation.
  • provide --> service
    Simple rename, works the same for the bundles of the converted product.
  • configurationPolicy --> configurationPolicy
    No rename but the options were upper-cased.
    Example:
    • 'configurationPolicy = ConfigurationPolicy.require'
      becomes
      'configurationPolicy = ConfigurationPolicy.REQUIRE'
  • servicefactory --> scope
    Works differently, check the javadoc of the OSGi R6 annotation to see which option is needed.
Note that this list is not exhaustive, it's just what was encountered in the conversion of the product.

For the example used earlier:
-@Component(provide = {})
+@Component(service = {})

6.2.2. @Reference


Some attributes of the OSGi R6 '@Reference' annotation are named differently or work differently than those of the bnd '@Reference' annotation.
  • type --> cardinality
    Works differently, check the javadoc of the OSGi R6 annotation to see which option is needed.
    Example:
    • 'type = '+''
      becomes
      'policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.AT_LEAST_ONE'.
Note that this list is not exhaustive, it's just what was encountered in the conversion of the product.

For the example used earlier:
-  @Reference(type = '+')
+  @Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.AT_LEAST_ONE)

6.3. Wrap-up


Compile, test and run the product; verifying that everything still works is a Very Good Idea.

When everything checks out then commit the changes into version control.


7. Conclusions


The conversion is not a difficult one for well structure code.
However, it is quite laborious, mistakes are easily made and a lot of details must be known and done right.

The result of the conversion is quite an improvement since a lot of the configuration handling code could be simplified as a result of no longer needing the bnd metatype helper.

Also, the OSGi R6 Declarative Services annotations seem a bit more to-the-point, self-explanatory and much better documented.

To top it all off, the bundles are reduced in size because the bndlib package for the metatype helper are no longer included in the bundles.


8. Postscript


Since you've made it this far, you should be well prepared to start converting your product!

If you have remarks, improvements or other comments then please leave them in the comments.

No comments:

Post a Comment