Dynamically configured declarative OSGi services

I’m creating OSGi services and found the need to create one or more instances of a service that was registered with user configured parameters.  It’s fairly straightforward to do this with the ConfigurationAdmin service, a ManagedServiceFactory and context.registerService(), but I’ve recently become a declarative services convert and wanted to do all of this with declarative services.  It turns out that you still use the ConfigurationAdmin service, just not the ManagedServiceFactory. In this post, I will demonstrate how to create a dynamically configured declarative service.

Example Use-case

Let’s say you are deploying an OSGi based server at multiple sites and your bundles need to access one or more databases using JDBC.  You create an OSGi service called DatabaseConnectionService that returns a JDBC Connection.  To create the connection to the database, you need configuration information typically consisting of: hostname, port number, database, user id, and password.  Since this information can change, and will vary from site to site where your server is deployed, the configuration information needs to be dynamically updatable and persistable.

Development Environment

You will need Eclipse 3.5, Equinox, and Apache Derby if you want to try this example on your own.  Create a target folder somewhere in your filesystem.  Extract the contents of the Equinox zip file into the target directory.  Create a derby folder as a child of target and extract the contents of the Derby zip into the derby folder.  The directory structure should look like:

Target platform directory structure

Target platform directory structure

Launch Eclipse 3.5 and select Preferences -> Plug-in Development -> Target Platform.  We need to add Equinox and Derby to our target platform.  Select Running Platform and click Edit… Add the Equinox and Derby folders to your target platform.  Your target platform should look like:

Target Definition

Target Definition

Unit Test

Create a new plug-in project and make sure the option is checked to generate an Activator.  In the Manifest editor, check Activate this plug-in when one of its classes is loaded.  Add org.junit4 to your required bundles and org.osgi.service.cm to your imported packages.  [Let’s not debate require bundle vs import package … just set up your dependencies however you like to get the junit tests to build.]

The unit test first gets the ConfigurationAdmin service from the bundle context.  I’ve taken a short-cut with my unit test for this example, and I’m relying on the fact that the ConfigurationAdmin service will be started before the unit test.  You should not rely on bundle start order in your production code.  Once you have the ConfigurationAdmin service, create a configuration and set the properties for your declarative service.  After you update the properties on the configuration, you will then have to wait for the service to be created.  Make sure you include a timeout so your unit test doesn’t hang forever in the event that the service is not started.  Your unit test will contain a compile error until you create the service interface.

package com.example.database.junit;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.junit.Assert.assertThat;

import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Hashtable;

import org.junit.Test;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.util.tracker.ServiceTracker;

import com.example.database.IDatabaseConnectionService;

public class TestDatabaseConnectionService
{
	@Test
	public void testCreateConnection() throws SQLException, IOException, InvalidSyntaxException, InterruptedException
	{
		BundleContext context = Activator.getDefault().getBundle().getBundleContext();

		ServiceReference configAdminReference = context.getServiceReference(ConfigurationAdmin.class.getName());
		assertThat(configAdminReference, is(notNullValue()));
		ConfigurationAdmin configAdmin = (ConfigurationAdmin) context.getService(configAdminReference);
		assertThat(configAdmin, is(notNullValue()));

		Configuration config = configAdmin.createFactoryConfiguration("com.example.database.connection", null);
		assertThat(config, is(notNullValue()));

		Hashtable<String, Object> properties = new Hashtable<String, Object>();
		properties.put("database.id", "example");
		properties.put("database", "junit");
		properties.put("user", "");
		properties.put("password", "");
		properties.put("create", "create");
		config.update(properties);

		ServiceTracker connectionServiceTracker = new ServiceTracker(context, IDatabaseConnectionService.class.getName(), null);
		connectionServiceTracker.open();
		IDatabaseConnectionService connectionService = (IDatabaseConnectionService) connectionServiceTracker.waitForService(1000);
		assertThat(connectionService, is(notNullValue()));
		Connection connection = connectionService.createConnection();
		assertThat(connection, is(notNullValue()));
		connection.close();
	}
}

Service Interface

Create a new plug-in project com.example.database and add the following interface:

package com.example.database;

import java.sql.Connection;
import java.sql.SQLException;

public interface IDatabaseConnectionService
{
	Connection createConnection() throws SQLException;
}

Edit your junit plug-in manifest to include a dependency on com.example.database.

Launch Configuration

Create a JUnit Plug-in Test launch configuration.  Make sure your test is selected, and that the Test runner is set to JUnit 4.

Launch Configuration (Test tab)

Launch Configuration (Test tab)

You will also need to set up the included plug-ins.

  1. Select the Plug-ins tab
  2. Select Launch with: plug-ins selected below only
  3. Click Deselect All
  4. Uncheck Include optional dependencies when computing required plug-ins
  5. Uncheck Add new workspace plug-ins to this launch configuration automatically
  6. Check com.example.database
  7. Check com.example.database.junit
  8. Check org.eclipse.equinox.cm
  9. Check org.eclipse.equinox.ds
  10. Click Add Required Plug-ins
  11. Set the Start Level on org.eclipse.equinox.cm to 3 and Auto Start to true

Launch Configuration (Plug-ins tab)

Launch Configuration (Plug-ins tab)

The start level of the ConfigurationAdmin bundle must be set to something lower than the declarative services bundle because of bug 276003.  Run your junit test, and it should fail on line 62.

Service Implementation

Now, lets create a real service that constructs a JDBC connection to derby.

package com.example.database;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Dictionary;

import org.apache.derby.jdbc.EmbeddedDataSource;
import org.osgi.service.component.ComponentContext;

public class DatabaseConnectionService implements IDatabaseConnectionService
{
	public Connection createConnection() throws SQLException
	{
		EmbeddedDataSource dataSource = new EmbeddedDataSource();
		dataSource.setDataSourceName(id);
		dataSource.setDatabaseName(database);
		dataSource.setUser(user);
		dataSource.setPassword(password);
		dataSource.setCreateDatabase(create);
		return dataSource.getConnection();
	}

	@SuppressWarnings("unchecked")
	protected void activate(ComponentContext context)
	{
		Dictionary<String, Object> properties = context.getProperties();
		id = (String) properties.get("database.id");
		database = (String) properties.get("database");
		user = (String) properties.get("user");
		password = (String) properties.get("password");
		create = (String) properties.get("create");
	}

	private String id;
	private String database;
	private String user;
	private String password;
	private String create;
}

Declare The Service

You are now ready to declare your service. Eclipse 3.5 contains new tooling for declarative services that makes this a snap.

  1. Right-click on your project and select New -> Folder
  2. Create a folder called OSGI-INF
  3. Right-click on the OSGI-INF folder and select New -> Other
  4. Select Plug-in Development -> Component Declaration
  5. Click Next
  6. Choose a file name such as component.xml
  7. Give your component a globally unique name such as com.example.database.connection
  8. For the Class, click Browse
  9. Choose DatabaseConnectionService
  10. Click Finish

The component definition editor will appear allowing you to set additional parameters and options. Set the Configuration Policy to require. This policy tells the service manager to not start the service component until it has a configuration from the ConfigurationAdmin service.  Check the options: This component is enabled when started and This component is immediately activated.

Component Options

Component Options

On the Services tab of the component editor, add the service interface as a provided service:

Component Services

Component Services

If you re-run your junit test, it should now pass.  If you want to dig a little deeper, set a breakpoint on line 55 of TestDatabaseConnectionService.java and run under the debugger.  When your breakpoint is triggered, switch to the console view and type in services (objectClass=com.example.database.IDatabaseConnectionService) and you will get information on your registered service including it’s configuration parameters.

OSGi Console

OSGi Console

That’s how you dynamically configure a declarative service!

Update: Simplified the unit test by using ServiceTracker.waitForService().

Advertisements

10 thoughts on “Dynamically configured declarative OSGi services

  1. Pingback: Remote declarative OSGi services « Miles To Code Before I Sleep

  2. Hi,

    this is a very exiting example. However, I tried to reproduce it with no success.

    The test fails when trying to receive the ServiceReference
    ( configAdminReference = context.getServiceReference(ConfigurationAdmin.class.getName());)

    A zipped set of eclipse example projects would be very helpfull for analysis.

    • Volkmar, check that org.eclipse.equinox.cm is included in your launch configuration, that its start level is set to 3, and auto-start is set to true. Press the Validate Plug-ins button and verify there are no dependency problems. You can also set a breakpoint on the failing line and run under the debugger. When you hit the breakpoint, issue the following command on the osgi console:

      ss cm

      Verify that the cm bundle is in the ACTIVE state.

  3. bryanhunt,
    thank you. I found the reason for the failure. The org.eclipse.equinox.ds was not added in the junit launch configuration.

    Volkmar

  4. I found it here: http://download.eclipse.org/equinox/drops/R-3.5-200906111540/index.php

    I used this example directly in a BundleActivator. Seems that the bundle is not started automatically when using ConfigurationAdmin. Is it normal?

    Another question. At OSGi API i found this: “The main purpose of this interface is to store bundle configuration data persistently”. The configuration is going to be stored somewhere?

    Thanks for this post. Very good!

    • Starting the BundleActivator automatically when one of its classes is loaded requires the following in the MANIFEST.MF:

      Bundle-ActivationPolicy: lazy

      If you are using the Eclipse IDE, you can set this in the manifest editor on the Overview tab. Check the box “Activate this plug-in when one of its classes is loaded”. Yes, the configuration will be stored in the configuration area. When you restart your application, OSGi will read the configuration and restart your services automatically with the stored configuration.

  5. Pingback: Configuring OSGi Services with Apache Web Console and Metatype « Miles To Code Before I Sleep

  6. Hello Bryan,
    Congrats on a good blog.

    Considering the nuances of Service Trackers and the fact that Eclipse is moving towards e4 where an Activators is NOT required, could you update/create a blog of this one without the use of the Activator.

    Thanks
    St Clair

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s