Control OSGi DS Component Instances via Configuration Admin

8 minute read

While trying to clean up the OSGi services in the Eclipse Platform Runtime I came across the fact that singleton service instances are not always feasible. For example the fact that the localization is done on application level does not work in the context of RAP, where every user can have a different localization.

In my previous blog post I showed how to manage service instances with Declarative Services. In that scope I mainly showed the following scenarios:

  • one service instance per runtime
  • one service instance per bundle
  • one service instance per component/requestor
  • one service instance per request

For cases like the one in the RAP scenario, these four categories doesn’t match very well. We actually need something additionally like one service per session. But a session is nothing natural in the OSGi world. At least not as natural as it is in the context of a web application.

First I tried to find a solution using PROTOTYPE scoped services introduced with DS 1.3. But IMHO that approach doesn’t fit very well, as by default the services have bundle scope, unless the consumer specifies that a new instance is needed. Also the approach of creating service instances on demand by using a Factory Component or the DS 1.3 ComponentServiceObjects interface does not seem to be a good option in this case. The consumer is in charge of creating and destroying the instances, and he needs to be aware of that fact.

A session is mainly used to associate a set of states to someone (e.g. a user) over time. The localization setting of a user is a configuration value. And configurations for OSGi services are managed by the Configuration Admin. Having these things in mind and searching the web and digging through the OSGi Compendium Specification, I came across the Managed Service Factory and this blog post by Neil Bartlett (already quiet some years old).

To summarize the information in short, the idea is to create a new service instance per Component Configuration. So for every session a new Component Configuration needs to be created, which leads to the creation of a new Component Instance. Typically some unique identifier like the session ID needs to be added to the component properties, so it is possible to use filters based on that.

The Managed Service Factory description in the specification is hard to understand (at least for me), the tutorials that exist mainly focus on the usage without Declarative Services by implementing the corresponding interfaces, and the blog post by Neil unfortunately only covers half of the topic. Therefore I will try to explain how to create service instances for different configurations with a small example that is based on the previous tutorial.

The sources for this blog post can be found in my DS projects on GitHub:

Note:
I will try to bring in some Configuration Admin details at the corresponding places, but for more information in advance please have a look at my Configuring OSGi Declarative Services blog post.

Service Implementation

Let’s start by creating the service implementation. Implement the OneShot service interface and put it in the org.fipro.oneshot.provider bundle from the previous blog post.

@Component(
    configurationPid="org.fipro.oneshot.Borg",
    configurationPolicy=ConfigurationPolicy.REQUIRE) 
public class Borg implements OneShot {

    @interface BorgConfig { 
        String name() default ""; 
    }

    private static AtomicInteger instanceCounter = new AtomicInteger();
    private final int instanceNo; private String name;

    public Borg() { 
        instanceNo = instanceCounter.incrementAndGet(); 
    }

    @Activate
    void activate(BorgConfig config) { 
        this.name = config.name(); 
    }

    @Modified
    void modified(BorgConfig config) { 
        this.name = config.name(); 
    }

    @Override
    public void shoot(String target) { 
        System.out.println("Borg " + name + " #" + instanceNo + " of "+ instanceCounter.get()
            + " took orders and executed " + target); 
    }
}

You should notice the following with that implementation:

  • We specify a configuration PID so it is not necessary to use the fully qualified class name later. Remember: the configuration PID defaults to the configured name, which defaults to the fully qualified class name of the component class.
  • We set the configuration policy REQUIRE, so the component will only be satisfied and therefore activated once a matching configuration object is set by the Configuration Admin.
  • We create the Component Property Type BorgConfig for type safe access to the Configuration Properties (DS 1.3).
  • We add life cycle methods for activate to initially consume and modified to be able to change the configuration at runtime.

Configuration Creation

The next thing is to create a configuration. For this we need to have a look at the ConfigurationAdmin API. In my Configuring OSGi Declarative Services blog post I only talked about ConfigurationAdmin#getConfiguration(String, String). This is used to get or create the configuration of  a singleton service. For the configuration policy REQUIRE this means that a single Managed Service is created once the Configuration object is used by a requesting bundle. In such a case the Configuration Properties will contain the property service.pid with the value of the configuration PID.

To create and handle multiple service instances via Component Configuration, a different API needs to be used. For creating new Configuration objects there is ConfigurationAdmin#createFactoryConfiguration(String, String). This way a Managed Service Factory will be registered by the requesting bundle, which allows to create multiple Component Instances with different configurations. In this case the Configuration Properties will contain the property service.factoryPid with the value of the configuration PID and the service.pid with a unique value.

As it is not possible to mix Managed Services and Managed Service Factories with the same PID, another method needs to be used to access existing configurations. For this ConfigurationAdmin#listConfigurations(String) can be used. The parameter can be a filter and the result will be an array of Configuration objects that match the filter. The filter needs to be an LDAP filter that can test any Configuration Properties, including service.pid and service.factoryPid. The following snippet for example will only return existing Configuration objects for the Borg service when it was created via Managed Service Factory.

this.configAdmin.listConfigurations("(service.factoryPid=org.fipro.oneshot.Borg)")

The parameters of ConfigurationAdmin#getConfiguration(String, String) and ConfigurationAdmin#createFactoryConfiguration(String, String) are actually the same. The first parameter is the PID that needs to match the configuration PID of the component, the second is the location binding. It is best practice to use “?” as value for the location parameter.

Create the following console command in the org.fipro.oneshot.command bundle:

@Component(
    property= {
        "osgi.command.scope=fipro",
        "osgi.command.function=assimilate"},
    service=AssimilateCommand.class ) 
public class AssimilateCommand {

    @Reference ConfigurationAdmin configAdmin;

    public void assimilate(String soldier) { 
        assimilate(soldier, null); 
    }

    public void assimilate(String soldier, String newName) { 
        try {
            // filter to find the Borg created by the
            // Managed Service Factory with the given name 
            String filter = "(&(name=" + soldier + ")" + "(service.factoryPid=org.fipro.oneshot.Borg))"; 
            Configuration\[\] configurations = this.configAdmin.listConfigurations(filter);

            if (configurations == null || configurations.length == 0) {
                //create a new configuration 
                Configuration config = this.configAdmin.createFactoryConfiguration( "org.fipro.oneshot.Borg", "?"); 
                Hashtable<String, Object> map = new Hashtable<>(); 
                if (newName == null) { 
                    map.put("name", soldier); 
                    System.out.println("Assimilated " + soldier); 
                } else { 
                    map.put("name", newName); 
                    System.out.println("Assimilated " + soldier + " and named it " + newName); 
                } 
                config.update(map); 
            } else if (newName != null) {
                // update the existing configuration 
                Configuration config = configurations[0];
                // it is guaranteed by listConfigurations() that
                // only Configuration objects are returned with
                // non-null properties 
                Dictionary<String, Object> map = config.getProperties(); 
                map.put("name", newName); 
                config.update(map); 
                System.out.println(soldier + " already assimilated and renamed to " + newName); 
            } 
        } 
        catch (IOException | InvalidSyntaxException e1) {
            e1.printStackTrace(); 
        } 
    } 
}

In the above snippet name is used as the unique identifier for a created Component Instance. So the first thing is to check if there is already a Configuration object in the database for that name. This is done by using ConfigurationAdmin#listConfigurations(String) with an LDAP filter for the name and the Managed Service Factory with service.factoryPid=org.fipro.oneshot.Borg which is the value of the configuration PID we used for the Borg service component. If there is no configuration available for a Borg with the given name, a new Configuration object will be created, otherwise the existing one is updated.

Note: To verify the Configuration Properties you could extend the activate method of the Borg implementation to show them on the console like in the following snippet:

@Activate
void activate(BorgConfig config, Map<String, Object> properties) { 
    this.name = config.name(); 
    properties.forEach((k, v) -> { 
        System.out.println(k+"="+v); 
    }); 
}

Once a service instance is activated it should output all Configuration Properties, including the service.pid and service.factoryPid for the instance.

Note:
Some more information on that can be found in the enRoute documentation and of course in the specification.

Service Consumer

Finally we add the following execute command in the org.fipro.oneshot.command bundle to verify the instance creation:

@Component(
    property= {
        "osgi.command.scope=fipro",
        "osgi.command.function=execute"
    },
    service=ExecuteCommand.class ) 
public class ExecuteCommand {

    @Reference(target="(service.factoryPid=org.fipro.oneshot.Borg)")
    private volatile List<OneShot> borgs;

    public void execute(String target) { 
        for (ListIterator<OneShot> it = borgs.listIterator(borgs.size()); it.hasPrevious(); ) { 
            it.previous().shoot(target); 
        } 
    } 
}

For simplicity we have a dynamic reference to all available OneShot service instances that have the service.factoryPid=org.fipro.oneshot.Borg. As a short reminder on the DS 1.3 field strategy: if the type is a Collection the cardinality is 0..n, and marking it volatile specifies it to be a dynamic reluctant reference.

Starting the application and executing some assimilate and execute commands will show something similar to the following on the console:

g! assimilate Lars
Assimilated Lars
g! assimilate Simon
Assimilated Simon
g! execute Dirk
Borg Lars #1 of 2 took orders and executed Dirk
Borg Simon #2 of 2 took orders and executed Dirk
g! assimilate Lars Locutus
Lars already assimilated and renamed to Locutus
g! execute Dirk
Borg Locutus #1 of 2 took orders and executed Dirk
Borg Simon #2 of 2 took orders and executed Dirk

The first two assimilate calls create new Borg service instances. This is verified by the execute command. The following assimilate call renames an existing Borg, so no new service instance is created.

Now that I have learned about Managed Service Factories and how to use them with DS, I hope I am able to adapt that in the Eclipse Platform. So stay tuned for further DS news!

Updated: