Build lightweight OSGi applications with Eclipse

OSGi has been acting as a de facto industry standard to build dynamic modular systems in the Java™ world and many other fields. Using a series of correlative examples, this article demonstrates the processes, scenarios, solutions and practices to develop an OSGi application in Eclipse. Read further to gain a systematic understanding of the OSGi framework and core services.

Yuan Tao Sun (sunyuant@cn.ibm.com), Software Performance Analyst, IBM

Photo of Sun, Yuan TaoSun, Yuan Tao is a software performance analyst for the Lotus Shanghai performance team at IBM China Software Development Lab who focuses on measuring and improving performance of Lotus social products including Lotus Connections and LotusLive iNotes. Prior to joining the performance team, he was a software developer experienced in software design pattern, Eclipse RCP, and Java 2 Platform, Enterprise Edition application development.



25 October 2011

Also available in Chinese Japanese

Introduction

There are numerous requirements for building modular systems. In the Java world, as well as many other fields, the Open Services Gateway initiative (OSGi) has been recognized as a mature framework for modular systems, which include desktop applications, web applications, mobile applications, and middleware. OSGi offers an underlying infrastructure to develop modular, dynamic, and service-oriented applications. Most features and abilities are defined and offered through services in OSGi specification and implementation. By understanding the concepts and usage of several core OSGi services, we are able to utilize these services, as well as Eclipse IDE, to build lightweight modular applications meeting complicated requirements.


A sample application

The sample application is a data collector that retrieves data from different kinds of data sources and parses them into a unified, pre-defined data format for further processing. There are multiple systems with quite different data format definitions and ways to retrieve them. In one example, the application producer or owner expects third-party vendors to implement process logic for particular data sources based on API published by the application. Ideally, the data collector client is expected to integrate third-party code and run them without any changes on existing code, configuration, or deployment structure. This is a typical requirement in building a modular system and we will introduce how to accomplish it with OSGi core services.

To unlock the full power of OSGi, careful design of the application architecture is necessary, although it is not complicated. There have been multiple articles published discussing the design principles of OSGi applications and I encourage you to read through them. (See Resources.)

Figure 1 shows the architecture of a sample data collector application. This application is made up of three types of bundles: the data collector framework bundle, text parser bundles, and collector bundles.

Figure 1. Sample data collector application architecture
Diagram shows ow the Data Collector Framework Bundle has APIs for Text Parsing, Data Collectors and Configuration Wizards that interact with various collector and text parsing bundles

The framework bundle acts as the core component of the whole application. It is either a bundle (providing collector and wizard UI APIs for other bundles), or a client (consuming services from other functional bundles). By merging API and client bundles into one bundle, third-party data collector developers can import a minimum number of bundle jars into their IDE to implement APIs, then run and test the whole application in Eclipse. Collector bundles provide data collecting services and wizard page services. On the other hand, collector bundles also act as a client, invoking the text parser API provided by the framework bundle, for which a separate parser bundle will provide the service.

All these bundles are loosely coupled and interact with each other through OSGi core services. As a result, all the bundles except for the framework bundle could be added, removed, suspended, or upgraded easily even after the application is deployed. This is an important feature for an automated software delivery process.

Let's demonstrate the process and practices to implement and deploy a small but completely formed OSGi application.


Define the OSGi project layout

We need to create a new Plug-in Project for each bundle in Eclipse. The following assumes previous experience or knowledge regarding creating OSGi bundles in Eclipse. Additionally, be sure to download the latest release of Eclipse IDE as well as separate Equinox SDK. Eclipse 3.4 or above is recommended.

As described for Figure 1, there are at least three bundle projects required – the framework bundle, a text parser bundle, and a data collector bundle. Defining the project layout, such as package and folder hierarchy design, is very important. Let's take the framework bundle project as an example in Figure 2.

Figure 2. Sample project layout
Screenshot shows a directory structure of a typical project

The OSGI-INF folder is created to store all OSGi component definition files in this bundle. (The component concept will be introduced later.) Packages are created according to function categories. We need to plan packages that are visible to other bundles in the early stage to avoid bundles coupling heavily. In the sample framework bundle, the following two packages are exported.

  • dw.sample.dc.api – contains all APIs to be implemented.
  • dw.sample.dc.consts – contains constants and enums shared between bundles such as event parameters.

Inject services with OSGi Declarative Services

Modularity and service-oriented are key concepts in OSGi specifications. Exposing implementation classes is supported by the OSGi framework, but not recommended. Instead, from the programming perspective, bundles are expected to interact with each other through publishing and consuming services, or instances of classes. OSGi provides two approaches to accomplish this:

  • Registering and retrieving services in the service registry via BundleContext object provided by the OSGi service layer.
  • Leveraging the Declarative Services in the OSGi R4 specification.

Declarative Services (DS) declares the Component Model concept. A component is a Java object defined with a component definition file, which is used to provide services and retrieve referenced services.

The lifecycle of components is managed by the OSGi container. What's more, referenced service instances are dynamically injected into components. DS will handle service look-up, binding or unbinding to the component according to pre-defined strategy. As a result, developers are able to produce a highly dynamic system with neat code.

Create the first service component

Let's start with a simple case: the data collector framework bundle defines a text parser API and a text parser bundle provides default parser service.

In the framework bundle, define the service API in the exported dw.sample.dc.api package. See Listing 1.

Listing 1. Text parser API
public interface ITextParser {
public String parseText(String input);
}

In the text parser bundle project, the dw.sample.dc.api package must first be imported into the manifest in order to ensure the interface class can be loaded by the current bundle class loader. Second, create a class to implement the above interface.

The last, most important step is to define the implementation class as a component, which requires creating a component definition file and registering it in the bundle manifest. To simplify the process, let's make use of the wizard provided by Eclipse (it might not be available in old releases):

  1. Right click the OSGI-INF folder in the parser bundle and select New>Component Definition.
  2. Specify the implementation class file just created as the component class, as well as definition file and component names, then click Finish.
  3. In the graphic component definition configuration window, switch to the Services tab and specify the service interface class of the framework bundle as the provided service of this component. See Figure 3. below.
Figure 3. Specify the provided services
Screenshot shows the provided services configured to dw.sample.dc.api.ITextParser

The wizard will generate a new textParserComponent.xml and register the component in the bundle manifest, as seen in Listing 2.

Listing 2. New generated component configuration
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
  name="dw.sample.textparser.defaultparser">
   <implementation class="dw.sample.textparser.service.DataCollectorDefaultTextParser"/>
   <service>
      <provide interface="dw.sample.dc.api.ITextParser"/>
   </service>
</scr:component>

MANIFEST.MF
...
Service-Component: OSGI-INF/textParserComponent.xml

We have now implemented a simple but complete component that provides service at runtime when a client invokes the API of the framework bundle.

Next, we will demonstrate a more complicated component and introduce how to consume services from other components.

A more advanced case

Let's consider a scenario where users are supposed to interact with the data collector application via GUI, which means they are able to input required data following a series of wizard pages. They then launch the collecting process and watch the progress on the UI. Obviously, the framework does not know what data is required by third-party data collectors. To meet the requirement, data collector bundles also need to provide at least one configuration wizard page. It also requires the framework to integrate and present all the third-party wizards seamlessly and dynamically after the application is deployed on the user environment. Sounds cool, right? Let's see how to realize it by leveraging the power of OSGi Declarative Services. (See Figure 4.)

Figure 4. Dynamic wizard sequence
Diagram shows how Wizard group 1 passes through Wizard group 2 which passes to Wizard 4. Wizard 3 has been greyed out.

Like the previous case, an API needs be defined. For this case, we need to carefully design the interface to avoid enlarging the complexity of the wizard system while ensuring the API is capable of covering key functional requirements from the data collector side. Therefore, the API design needs to at least satisfy the following:

  • Do not allow any communication and dependencies among wizard services from different data collectors, but do not restrict them among ones within the same data collector.
  • Saving the user input at the stage when the user steps out of the last wizard page of a data collector.

This is the most important step in this case. A well-formed API design is to provide clear interfaces while keeping the service highly cohesive, which doesn't care about the logic and code complexity inside an implementation.

SWT and JFace are used as underlying libraries of the application wizard as well as other GUI components. If we plan to use resources and classes across multiple packages in these two libraries, simply add them as the required bundle in the manifest files of framework and data collector bundles. Also import the org.osgi.service.component package because we will invoke classes in this package later.

The wizard API defined in the framework bundle is made up of two classes as shown in Listing 3.

Listing 3. Define the wizard service API
public interface ICollectorWizardGenerator {
public List<CollectorWizardPage> getWizardSequence();
public int getWizardPositionIndex();
}

---
/**
 * Inherit the jface WizardPage by adding two customized methods
 */
public abstract class CollectorWizardPage extends WizardPage {
    …
/**
* Trigger the customized logic when the next button on the wizard is
 * pressed
 * 
 * @return whether to jump to next page.
 */
public abstract boolean nextPressedTrigger();

/**
* Save user input in the current wizard.
 */
public abstract void saveWizardInput();    
}

In the data collector bundle, implement the wizard API and define the class which implements the ICollectorWizardGenerator interface as the service component. (See Listing 4.)

Listing 4. The wizard service component
public class ProfileWizardGeneratorImpl implements ICollectorWizardGenerator {
    private int wizardPosition = 10;

    protected void activate(ComponentContext context) {
        try {
            wizardPosition = ((Integer) context.getProperties().get(
                    "wizardPosition")).intValue();
        } catch (NumberFormatException e) {
            wizardPosition = 10;
        }
    }
    public List<CollectorWizardPage> getWizardSequence() {
        List<CollectorWizardPage> wizardPageSeq = new ArrayList<CollectorWizardPage>();
        wizardPageSeq.add(new ProfileCollectorWizardPage1());
        wizardPageSeq.add(new ProfileCollectorWizardPage2());
        return wizardPageSeq;
    }
    public int getWizardPositionIndex() {
        return wizardPosition;
    }
}

---
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" 
  name="dw.sample.dc.profile.wizardgenerator">
   <implementation class="dw.sample.dc.profile.service.ProfileWizardGeneratorImpl"/>   
   <service>
      <provide interface="dw.sample.dc.api.ICollectorWizardGenerator"/>
   </service>
   <property name="wizardPosition" type="Integer" value="2"/>
</scr:component>

Notice that the activate method is overridden in the component class. The method as well as the deactivate method are invoked by the OSGi framework when the component is activated or deactivated. The ComponentContext object is passed to the activate method so that we can get component information like component properties defined in the XML file. Here the data collector vendor can define the wizard position index thus the framework bundle could arrange the wizard appearance sequence accordingly.

So far we have made two components as service providers.

Consume services

In the Declarative Services model, services are dynamically injected into components. The code in Listing 5. below demonstrates how to refer to services and consume them in the bundle.

Listing 5. The parser service loader component
public class TextParserLoader {
    private static TextParserLoader instance;
    ... ...
    private ITextParser parser;

    public static TextParserLoader getInstance() {
        return instance;
    }
    protected void activate(ComponentContext context) {
        try {
            locker.lock();
            instance = this;
        } finally {
            locker.unlock();
        }
    }
    public void setTextParser(ITextParser parser) {
        this.parser = parser;
    }
    public void unsetTextParser(ITextParser parser) {
        if (this.parser != parser) {
            return;
        }
        this.parser = null;
    }
    public ITextParser getTextParser() {
        return parser;
    }
}

---
<implementation class="dw.sample.dc.profile.service.TextParserLoader"/>
<reference bind="setTextParser" cardinality="1..1" 
  interface="dw.sample.dc.api.ITextParser" name="ITextParser" 
policy="dynamic" unbind="unsetTextParser"/>

Here the singleton pattern is applied to the component class. Other non-component objects in the bundle are able to get and consume services from the unique TextParserLoader instance while getting rid of OSGi-specific code. The loader class somehow acts as what a factory class plays in traditional Java applications, but the magic is that it need not handle any bothersome class loading, reflection and instance initiation.

DS supports different service reference strategies defined in the component XML file:

  • Policy: The policy property has two values – static and dynamic. The static policy is default which means the component will be re-activated once the referred services have any changes. Instead, the dynamic policy just invokes the pre-defined bind and unbind methods so it is more highly recommended.
  • Cardinality: the property has four available values: 1..1, 1..n, 0..1, 0..n. Given there might be multiple services available for the same API, the end number indicates whether all services or just one of them will be injected into the component and the start number indicates whether available services are mandatory to activate the component.

The 1..1 cardinality is the default value and we have seen how it was used from the above example. For the 0..n case, it can be also well-handled along with the singleton pattern, as shown in Listing 6.

Listing 6. Inject multiple services
public class DataCollectorWizardSequence {
    ...
    private List<ICollectorWizardGenerator> wizardGeneratorSeq = 
	                            new ArrayList<ICollectorWizardGenerator>();
    ...
    public void addCollectorWizardGenerator(ICollectorWizardGenerator wizardGenerator) {
        wizardGeneratorSeq.add(wizardGenerator);        
    }
    public void removeCollectorWizardGenerator(ICollectorWizardGenerator wizardGenerator)
    {        
        wizardGeneratorSeq.remove(wizardGenerator);        
    }
    public List<CollectorWizardPage> getDataCollectorWizardSequence() {
        List<CollectorWizardPage> collectorWizardSequence = 
		                    new ArrayList<CollectorWizardPage>();
        try {
            locker.lock();
            /*
             * Arrange the wizard sequence according to position indexes 
	    * defined by each wizard services
	    */
            Collections.sort(wizardGeneratorSeq,
                    new CollectorWizardSequenceComparator());
            for (ICollectorWizardGenerator generator : wizardGeneratorSeq) {
                collectorWizardSequence.addAll(generator.getWizardSequence());
            }
            return collectorWizardSequence;
        }
        finally {
            locker.unlock();
        }
    }
}

---
<reference bind="addCollectorWizardGenerator" cardinality="0..n" 
       interface="dw.sample.dc.api.ICollectorWizardGenerator" 
	   name="ICollectorWizardGenerator" policy="dynamic" 
	   unbind="removeCollectorWizardGenerator"/>

Once we understand how to leverage components introduced in the Declarative Services to publish and consume services, let's learn how to make effective use of other OSGi core services with this model.


Manage bundle configuration with the Configuration Admin Service

The Configuration Admin Service provides a unified approach for OSGi applications to manage local or remote configuration data either on a service level or on a bundle level. We will begin with the most common case – to store, retrieve, and update bundle configuration data locally.

First, check whether the org.eclipse.equinox.cm_<version>.jar is in the Eclipse plugins folder, which is the Equinox implementation for the OSGi Configuration Admin Service. Otherwise, place it there from the separately downloaded Equinox SDK. Then import the org.eclipse.equinox.cm package into the target bundle manifest.

To use the Configuration Admin Service, first, and most importantly, the ManagedService API must be implemented and published as a service by the target bundle. There is only one method updated to be implemented. Different ManagedServices are distinguished by SERVICE_PID registered with services, thus corresponding service will be triggered when its configuration is changed. To manage the bundle-level configuration, an appropriate object to provide the ManagedService is the bundle activator as in Listing 7.

Listing 7. The bundle activator providing ManagedService
public class Activator implements BundleActivator {
    private ServiceRegistration cmSvrReg;

    public void start(BundleContext context) throws Exception {
cmSvrReg = context.registerService(ManagedService.class.getName(),
                    this, initMgrServiceProp());
}
    public void stop(BundleContext context) throws Exception {
        cmSvrReg.unregister();
    }
    public void updated(Dictionary properties) throws ConfigurationException {
        if (cmSvrReg == null) {
            return;
        }
        if (properties == null) {
            cmSvrReg.setProperties(initMgrServiceProp());
        } else {
            cmSvrReg.setProperties(properties);
        }
    }
    public static Dictionary initMgrServiceProp() {
        Dictionary result = new Hashtable();
        // Suppose that the Activator class name is unique across bundles…
result.put(Constants.SERVICE_PID, Activator.class.getName());
        return result;
    }
}

Next, as shown in Listing 8, create a component loading the Configuration Admin Service published by the OSGi framework.

Listing 8. The ConfigManager component
public class DCFrameworkConfigManager {
    private ConfigurationAdmin cm;
    ...
    protected void activate(ComponentContext context) {
        ...
        Configuration config = getFrameworkConfig();
try {
            writeLock.lock();
            if (config.getProperties() == null) {
                config.update(context.getProperties());
            }
        } finally {
            writeLock.unlock();
        }
    }
    public void setConfigAdmin(ConfigurationAdmin cm) {
        this.cm = cm;
    }
    public void unsetConfigAdmin(ConfigurationAdmin cm) {
        this.cm = null;
    }
    public Configuration getFrameworkConfig() throws IOException {
        try {
	readLock.lock();
            return cm.getConfiguration(Activator.class.getName());
        } finally {
            readLock.unlock();
        }
    }
    public void updateFrameworkConfig(Dictionary<String, String> properties)
            throws IOException {
        try {
            writeLock.lock();
            Configuration config = getFrameworkConfig();
            if (config != null) {
                config.update(properties);
            }
        } finally {
            writeLock.unlock();
        }
    }
}

---
<reference bind="setConfigAdmin" cardinality="1..1" 
    interface="org.osgi.service.cm.ConfigurationAdmin" name="ConfigurationAdmin" 
    policy="dynamic" unbind="unsetConfigAdmin"/>
<properties entry="OSGI-INF/initConfig.properties"/>

After activation, the ConfigManager component initiates the bundle configuration from a property file and provides methods which can be used anywhere within the bundle to read and save the configuration at runtime. They are protected by locks to handle the concurrent invoking case.


Handle events with the Event Admin Service

For many systems, it is essential to implement an event handling model. OSGi's service-oriented framework provides the implementation mechanism through the Event Admin Service.

To make use of the Event Admin Service correctly, remember three things:

  • The event publisher needs leverage referenced EventAdmin service to send the Event object.
  • The event subscriber provides the EventHandler service by implementing the handleEvent method to handle received events.
  • The event.topics property of the Event object determines which event handlers can receive the event.

The OSGi Event Admin Service implements a traditional event handling programming model in a more dynamic way, yet doesn't introduce new concepts into the model. Like other OSGi services introduced in this article, we will retrieve the service through component objects. Declarative Services support a component to be both service provider and service consumer, which makes it possible for a component to become both an event publisher and an event handler at the same time.

Consider this requirement: Users should be able to watch the data collecting progress on the application UI and stop the process at any time via the UI control. For a data collector bundle, technically it means there should be bi-channel events sent and handled at both the application framework bundle and the data collector bundle sides. (See Listing 9 below.)

Let's just take the data collector bundle side as an example. Again, first check the Eclipse plugins folder, copy the org.eclipse.equinox.event_<version>.jar if it isn't there, and import the package into the bundle manifest.

Listing 9. The event publisher and handler component
public class ProfileDataCollectorImpl implements IDataCollector, EventHandler {
    public final static String EVENT_TOPIC =
                    "dw/sample/dc/event/collector/ProfileDataCollectorImpl/status";
    private EventAdmin eventAdmin;
    ...
    public int collectAndOutput() {        		
        while (...) {
            if (isCancelled()) {
                cancelProcess(false);
                return -1;
            }
            ...
            publishEvent(GUIMsg.STATUS_AND_PROGRESS.flag(), 
                "Collecting profile data...", 0, true);
            ...
        }
        ...
    }
    private void publishEvent(int status, String statusMsg, int progress,
        boolean async) {
        Dictionary<String, String> props = new Hashtable<String, String>();
        props.put(FrameworkEventConsts.PARAM_STATUS, String.valueOf(status));
        props.put(FrameworkEventConsts.PARAM_STATUS_MSG, statusMsg);
        props.put(FrameworkEventConsts.PARAM_PROGRESS, String.valueOf(progress));
        if (!async) {
            eventAdmin.sendEvent(new Event(EVENT_TOPIC, props));
        } else {
            eventAdmin.postEvent(new Event(EVENT_TOPIC, props));
        }
    }
    public void handleEvent(Event event) {
        String action = (String) event
                .getProperty(FrameworkEventConsts.PARAM_ACTION);
        if (FrameworkEventConsts.VAL_CANCEL.equalsIgnoreCase(action)) {
            cancelProcess(true);
        }
    }
    ...
}

---
<property name="event.topics" type="String" 
    value="dw/sample/dc/event/framework/UserCmdEventPublisher/uicmd"/>
<service>
   <provide interface="org.osgi.service.event.EventHandler"/>
   <provide interface="dw.sample.dc.api.IDataCollector"/>
</service>
<reference bind="setEventAdmin" cardinality="1..1" 
    interface="org.osgi.service.event.EventAdmin" name="EventAdmin" policy="dynamic" 
    unbind="unsetEventAdmin"/>

In this example, notice the format of the event.topics property value. Also, we can see the Event Admin Service allows developers defining the data to be transmitted via the Event object.


Specify the launch model with the Application Admin Service

So far we have covered the major aspects of developing an OSGi application and the sample application is ready to run. As Figure 1. show, a native OSGi application is made up of bundles running on top of the OSGi container. By default, the OSGi console will show up when the application is launched. There are no problems if it runs at the server side. If it is supposed to be delivered as a client application, however, the question arises of how to package the application so that it is able to perform just like a modern application, i.e., to launch in a more 'professional' way while keeping the modularity and dynamic nature inside.

Here we introduce a simple method by implementing the Equinox application model based on the OSGi Application Admin Service.

The first step is to add the org.eclipse.equinox.app bundle as the Require-bundle in the manifest of the application framework bundle. Create a class implementing the IApplication interface as in Listing 10.

Listing 10. Implement the IApplication interface
public class GUIApplication implements IApplication {    
    public Object start(IApplicationContext context) {
        // Invoke the entry GUI object here
        ...
        return IApplication.EXIT_OK;        
    }
    public void stop() {
        // Add the logic when the application quits.
    }
}

Second, define the application framework bundle as an Eclipse application by declaring the extension in the plugin.xml as in Listing 11.

Listing 11. plugin.xml
<plugin>
    <extension id="GUIApp"
         point="org.eclipse.core.runtime.applications">
      <application cardinality="1">
         <run
               class="dw.sample.dc.launcher.GUIApplication">
         </run>
      </application>
   </extension>      
</plugin>

The application cardinality specifies that the application can only have one instance at a time. Since the bundle has declared an extension, it is required to append the singleton directive in the Bundle-Symbolic manifest header like:

Bundle-SymbolicName: dw.sample.dc;singleton:=true

Next, create a new configuration to launch the OSGi application in the Eclipse IDE. (See Figure 5.)

Figure 5. Create a new configuration
Screenshot shows adding arguments to a new configuration

On the Bundles tab, we need click Add Required Bundles to include all mandatory dependencies as the platform bundles. On the Arguments tab, we need to add two new arguments to enable the application model. Notice that the value of -application argument should be in the form of: "bundle symbolicname"."extension id".

Once we are able to run the application and verify all the functions work as expected in Eclipse, it indicates we have finished the development work to build an OSGi application. Therefore, we can export each bundle project to a jar file, deploy them, and run integrated tests in the real environment, which is beyond the topic of this article.


Summary

This article demonstrates detailed steps for making use of OSGi core services to develop and launch a modular application in Eclipse. Examples in this article show that the Service-Oriented Component Model coming up with Declarative Services is playing an important role of helping developers make effective use of other OSGi core services to improve the dynamics of modular applications.

Resources

Learn

Get products and technologies

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Open source on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Open source, Information Management
ArticleID=767187
ArticleTitle=Build lightweight OSGi applications with Eclipse
publish-date=10252011