OSGi in a Nutshell: Working with service configurations

Today we will see a slightly more complicated version of the use case we saw last week. We will always talk about services, but of configurable services.

Use Case

Imagine you have two or more implementations for the same service, and you have a consumer which needs one of your implementation at a certain point, and the other one at another point. Or, maybe you have more consumers, and some of them need the first implementation of your service, while some others need the second one.

For instance, let’s suppose you want to provide a service which allows to export some data into a file. Suppose your client does not want to be limited to just one file format to see his data, but would like to be able to export them both in .ods and .txt. The contract is basically the same in both cases: he wants to export some data into a file. But the way you achieve one or the other (an export to an .ods and to .txt) is probably different, right? You would need, then, two different implementations! Then in the UI, you probably would like to associate to an Export to .ods button the first implementation and to an Export to .txt button the second one.

How can we handle such situation? How can we distinguish among different implementations of the same service interface? This is where configurations come into play!

Configurations

You can think of a configuration simply as a list of properties, where each property is a key/value pair. And these properties are just labels for our service implementations, a sort of schematic description of what an implementation can handle, compared to another one.

Coming back to our data export example, a good property name to distinguish between the two implementations would be format, which could then be equal to ods for the first implementation and to txt for the second one.

You got the idea, right? Then let’s see how we can work with service configurations in plain Java and then using OSGi!

Pure Java

In Java, common practice is to place your configurations in a .properties file. So, in our case we could write two .properties files. The first one, let’s call it export2ods.properties will contain something like:

format=ods

while the second one, export2txt.properties will be:

format=txt

Now, let’s create a Java Project with our service interface, our two implementations and a resources folder containing these properties files.

usecase2_1

Our service interface just defines the contract:

package test.service.config.pure.java.api;

public interface MyExportService {
    String export();
    String getFormat();
}

while our two implementations will look something like:


package test.service.config.pure.java.impl;

import java.util.Properties;
import test.service.config.pure.java.api.MyExportService;
import test.service.config.pure.java.helper.ConfigHelper;

public class MyODSExportImpl implements MyExportService {

    ConfigHelper helper = new ConfigHelper("export2ods.properties");
    Properties prop = helper.getConfigProperties();

    @Override
    public String export() {

//      implementation details for actually exporting something into ods

        if(prop != null) {
            return "Exported to " + prop.getProperty("format") + " file";
        }
        return null;
    }

    @Override
    public String getFormat() {
        return prop.getProperty("format");
    }
}


package test.service.config.pure.java.impl;

import java.util.Properties;
import test.service.config.pure.java.api.MyExportService;
import test.service.config.pure.java.helper.ConfigHelper;

public class MyTxtExportImpl implements MyExportService {

    ConfigHelper helper = new ConfigHelper("export2txt.properties");

    Properties prop= helper.getConfigProperties();

    @Override
    public String export() {

//      implementation details for actually exporting something into txt

        if(prop != null) {
            return "Exported to " + prop.getProperty("format") + " file";
        }
        return null;
    }

    @Override
    public String getFormat() {
        return prop.getProperty("format");
    }
}

We used here an helper class, which takes as input parameter for the constructor the filename of the .properties file corresponding to the service implementation, and actually loads the configuration, allowing us to print the message after the export is done.


package test.service.config.pure.java.helper;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class ConfigHelper {

    private String configFile;

    public ConfigHelper(String configFile) {
        this.configFile = configFile;
    }

    public Properties getConfigProperties() {

        try(InputStream is =
                getClass().getClassLoader().getResourceAsStream(configFile)) {
            Properties prop = new Properties();
            prop.load(is);
            return prop;
        } catch(IOException e) {
            System.out.println("Error loading InputStream " + e.getMessage());
            return null;
        }
    }
}

We can then package our project into a .jar file, not forgetting to include also the folder containing the .properties files, and we can then move to our consumer project.

The consumer is very similar to the one we saw last week, in the sense that we can make use of the Java ServiceLoader to look for all the implementations of our service. In this case we expect two different implementations, and we want to differentiate when we find one with respect to when we find the other, so we can write something like:


package test.consumer.config.pure.java;

import java.util.ServiceLoader;
import test.service.config.pure.java.api.MyExportService;

public class MyServiceConsumer {

    public static void main(String[] args) {
        ServiceLoader<MyExportService> loader = ServiceLoader.load(MyExportService.class);
        for(MyExportService service : loader) {
            if("ods".equals(service.getFormat())) {
                System.out.println("Starting exporting to ods");
                System.out.println(service.export());
            }
            else if("txt".equals(service.getFormat())) {
                System.out.println("Starting exporting to txt");
                System.out.println(service.export());
            }
        }
    }
}

When we run MyServiceConsumer as a Java Application we should see something like:

Starting exporting to ods
Exported to ods file
Starting exporting to txt
Exported to txt file

as proof that both our service implementations have been loaded.

With OSGi (and bnd)

Let’s see now how we can achieve the same thing in an OSGi environment.

We can use, as we did for last week use case, our Gecko Templates, which you can use by adding to your dependencies:


<dependency>
  <groupId>org.gecko.runtime</groupId>
  <artifactId>org.gecko.templates</artifactId>
  <version>1.0.31</version>
</dependency>

We can use the template Component with API Development to generate our OSGi bnd project for our service and implementations.

Our service interface will look like:


package test.osgi.bnd.service.config.case1.api;

import org.osgi.annotation.versioning.ProviderType;

@ProviderType
public interface MyExportService{
    String export();
}

while, our two implementations:


package test.osgi.bnd.service.config.case1.impl;

import org.osgi.service.component.annotations.Component;
import test.osgi.bnd.service.config.case1.api.MyExportService;

@Component(service = MyExportService.class, property = "format=ods")
public class MyODSExportImpl implements MyExportService{

    /*
     * (non-Javadoc)
     * @see test.osgi.bnd.service.config.case1.api.MyExportService#export()
     */
    @Override
    public String export() {

//      Implementation details to export in ODS
        return "Finished exporting to ODS";
    }
}


package test.osgi.bnd.service.config.case1.impl;

import org.osgi.service.component.annotations.Component;
import test.osgi.bnd.service.config.case1.api.MyExportService;

@Component(service = MyExportService.class, property = "format=txt")
public class MyTxtExportImpl implements MyExportService{

    /*
     * (non-Javadoc)
     * @see test.osgi.bnd.service.config.case1.api.MyExportService#export()
     */
    @Override
    public String export() {

//      Implementation details to export in ODS 
        return "Finished exporting to TXT";
    }
}

The important thing to notice here is that we are not using any .properties files, but we are defining the properties of each implementation through the element property of the @Component annotation.

When creating our service consumer, we can use the the template Component Development, and in our MyServiceConsumer we can inject both the service implementations like:


package test.osgi.bnd.case1.consumer;

import org.osgi.service.component.annotations.*;
import test.osgi.bnd.service.config.case1.api.MyExportService;

@Component(immediate = true)
public class MyServiceConsumer {

    @Reference(target = "(format=ods)")
    MyExportService myODSExport;

    @Reference(target = "(format=txt)")
    MyExportService myTxtExport;

    @Activate
    public void activate() {
        System.out.println("MyConsumer is active");

        System.out.println("Exporting to ODS");
        System.out.println(myODSExport.export());

        System.out.println("Exporting to txt");
        System.out.println(myTxtExport.export());
    }
}

As you can see, this time we select which kind of MyExportService we want through the element target of the @Reference component. The value of this element is a LDAP filter. For instance, if you have a service with multiple properties and you want to select one implementation based on two of these properties, the value of target should be something like:


&(prop1=value1)(prop2=value2)

For more information on the LDAP filter syntax, please visit here.

In the generated .bndrun file, add the dependencies, resolve and run.

img

and in the console you should see that both the implementations are correctly injected and call.

img

Using the Configurator

When you have more complex services, which may be rely on a large set of properties, then the previously discussed approach could become a bit unpractical. You would need to write quite a lot of things inside the annotations and then your code would become quite dirty. Or, you could have a lot of services which just differ for a few properties but whose implementation looks very similar. Then you do not want to code a different implementation for all of them, right?

These cases can be addressed in OSGi with a configurator.

Let’s generate a second Component with API Development project from our templates, and this time let’s add an additional config folder.

Let’s then create an interface and just one implementation.


package test.osgi.bnd.service.config.case2.api;

import org.osgi.annotation.versioning.ProviderType;

@ProviderType
public interface MyMessageService{

    String getMessage();
}


package test.osgi.bnd.service.config.case2.impl;

import org.osgi.service.component.annotations.*;
import test.osgi.bnd.service.config.case2.api.MyMessageService;

@Component(configurationPid = "MyMessageService")
public class MyMessageServiceImpl implements MyMessageService{

    MsgConfig config;

    @interface MsgConfig {
        String name() default "Unknown";
        String greetings() default "Hi";
        String nextApp() default "tomorrow";
    }

    @Activate
    public void activate(MsgConfig config) {
        this.config = config;
    }

    /*
     * (non-Javadoc)
     * @see test.osgi.bnd.service.config.case2.api.MyMessageService#getMessage()
     */
    @Override
    public String getMessage() {
        String msg = config.greetings() + " " + config.name() + "," + config.nextApp();
        return msg;
    }
}

A lot of things are going on here. Let’s try to understand them.

  • We set the configurationPid element in the @Component annotation. This is like a prefix for all the service instances that we want to create and that should use this implementation;
  • Then we define a MsgConfig annotation, with all the properties we need for the service. As you can see we also set some default values, in case some property is not found;
  • In the activation method we pass a MsgConfig as parameter and we set that to our instance variable, in order to use it in the getMessage() method.

But, where this configuration comes from? Where do we set a value different from the default ones? Well, we can create a config.json file in our config folder. For instance:


{
    ":configurator:resource-version": 1,
    "MyMessageService~polite":
    {
        "greetings" : "Dear",
        "name": "customer",
        "nextApp": "we would like to inform you that your next appointment is tomorrow."
    },
    "MyMessageService~friendly":
    {
        "greetings" : "Hi",
        "name": "dude",
        "nextApp": "see you tomorrow!"
    }
}

In this file we are setting the properties for two instances of the MyMessageService which should use the MyMessageServiceImpl implementation. You can see that because for both of them we used the same configurationPid value that we set in the implementation (MyMessageService). The part that comes after the ~ is the actual identifier of the service. In this case we have two, polite and friendly. Then comes the actual configuration, where you can identify the same properties we used in the MsgConfig in the implementation.

But how exactly is this sufficient to create two instances of MyMessageService? Well, it’s not, actually. We still need a simple addition. In the impl.bnd file, we need to add the OSGi configurator and to include the config folder to the resources.

img

With these directives, we are saying to the OSGi framework to look in the config folder and see whether it can find a configuration for which it knows what to do, namely for which we have an implementation.

Let’s create our usual consumer component.


package test.osgi.bnd.case2.consumer;

import org.osgi.service.component.annotations.*;
import test.osgi.bnd.service.config.case2.api.MyMessageService;

@Component(immediate = true, service = MyServiceConsumer.class)
public class MyServiceConsumer {

    @Reference(target="(service.pid=MyMessageService~polite)")
    MyMessageService politeMsgService;

    @Reference(target="(service.pid=MyMessageService~friendly)")
    MyMessageService friendlyMsgService;

    @Activate
    public void activate() {
        System.out.println("-------------");
        System.out.println("Polite Msg: " + politeMsgService.getMessage());
        System.out.println("-------------");
        System.out.println("Friendly Msg: " + friendlyMsgService.getMessage());
    }
}

This time, to select the two services we want we used the service.pid property and we set its value to the complete PID of the services, namely the configurationPid plus ~ and the unique identifier.

This time, in the .bndrun file, do not forget to include also the implementation package. When you press Resolve you should see something similar to this:

img

As you can see, two additional dependencies have been calculated by bnd: org.apache.felix.configadmin and org.apache.felix.configurator.

Indeed is the OSGi ConfigAdmin which is responsible to look for the different configurations files and trigger the creation of the various component instances.

When you run it you should see the two types of messages printed in the console:

img

We have already discussed that OSGi is a dynamic framework, right? So, what happens if we remove one of the two configurations from the .json file, while the application is running?

Let’s try! Simply remove one of the two configurations from the file and save the changes. You should see the application reloading automatically in the console, but this time no message is printed, of neither the services. Why is that? Well, we are injecting both the services into our consumer, but one of them is not found because we do not have any configuration that matches anymore for it, and so also the consumer is not activated!

If you type in the console list, you should see the different services, and you should see that the service consumer has UNSATISFIED REFERENCE. Try to type info plus the service id and you should get some more details on which reference is actually unsatisfied. Hopefully, this should be the one corresponding to the configuration you just removed!

img

Collecting all your Configurations

When you have a larger application. it is very likely that you would need a lot of configurations at runtime. What you could do, is to create a bundle which is responsible to collect all these configurations. Our Gecko Templates offer such a project, which is called Configurator.

img

If you generate a new project using such template you should get something like:

img

As you can see, nothing is generated in the src folder this time, but you have already the configs folder which contains a sample config.json file. In this folder you can place all the configuration files you need for your application. The generated .bnd file has already the directives to include the configs folder and to require the OSGi configurator.

img

This concludes our discussion on configurable services! Stay tuned for our next episode of the series!

by Ilenia Salvadori