Tuesday, January 24, 2012

Whirlwind Tour of OSGi: Using Maven for Bundle Packaging, Activators, Service Registration, Configuration, and Custom Shell Commands

OSGi, without the proper assistance, can be a pretty complex technology for engineers to learn.  There's a lot of things to know in order to be productive, and worse yet, most of the process is a departure from what Java developers are accustomed to.  I'm going to attempt to demonstrate how to do some very basic things to become immediately productive in OSGi, including:
  1. Setting up an OSGi bundle project in Maven.
  2. Importing the project into Eclipse.
  3. Configuring Maven and Adding Dependencies.
  4. Writing an OSGi Activator.
  5. Compiling/Creating the Bundle.
  6. Registering a Service in OSGi.
  7. Consuming a Service in OSGi.
  8. Consuming Configuration Updates.
  9. Writing Menu Extensions for the Apache Felix Gogo shell.
  10. Dynamically configuring a Service using the Gogo shell.

I’m not going to spend any time discussing what OSGi is or the importance of the technology.  If you would like to understand the impetus for the framework, I suggest reading the Wikipedia article.  Before starting this tutorial, you should understand what a "bundle" is and the general lifecycle of bundles within an OSGI container.

1.  Setting up an OSGi bundle project in Maven.

Let's start by setting up the project structure in Maven.  I'm going to walk the reader through the correct steps of setting up Maven to do all of the "bundle" work.

Start by creating a project using the "quickstart" archetype.  (I ran this command in the root folder of my Eclipse workspace).
mvn archetype:generate \
 -DgroupId=com.berico.time \
 -DartifactId=timeprovider \
 -Dpackage=com.berico.timeprovider \
 -Dversion=0.0.1-SNAPSHOT
After executing this command, Maven's going to dump 534 different archetypes to choose from.  The default archetype is the Maven "quickstart" archetype (# 169 at the time of this writing).  You can enter 169, or just hit enter.  In all following questions, just use the default (by pressing enter).

2.  Importing the project into Eclipse.

We need to "Eclipsify" your project.  This will let you import the project into Eclipse.  This command also helps sometimes when Eclipse starts messing up the Maven dependency management process (I find that my Eclipse instance stops refreshing the Maven dependencies and this is the only fix).
cd timeprovider
mvn eclipse:eclipse
Now we need to import the project through the IDE.  First start by going to File and selecting Import...


Then we select the Existing Projects into Workspace option and press the Next > button.


When the next window pops up, press the Browse button and select the directory created by Maven and then press Open.


Eclipse should accept the directory as an Eclipse project.  If you did not perform the mvn eclipse:eclipse, it will not allow you to import the directory as a project.  Select Finish to continue.


In the Project Explorer, pane to the left (in Java Perspective), you should see your Maven generated project with the source folders and the pom.xml file.



The last thing we want to do is allow Maven to control the dependencies within Eclipse.  If you are using the SpringSource Tool Suite, the Maven plugin should be installed.  Otherwise, you will need to go through the process of installing it (I think it's called "m2eclipse").

Right-click the project, navigate to the Maven submenu and select Enable Dependency Management.


Ok, we've setup Eclipse and are now ready to configure Maven.

3.  Configuring Maven and Adding Dependencies.

Now that we have the project imported into Eclipse, let's set up Maven to support the creation of OSGi bundles.  The Apache Felix project includes a plugin for Maven which makes developing OSGi bundles a lot easier.  We are going to modify the Maven pom.xml file to include this plugin, as well as, adding a couple of dependencies we will need for our module.

First, let's add the dependencies.  For the purpose of this project, we want the following libraries:

  • org.osgi.core - this is the model necessary to write OSGi applications.  We will get this from the Apache Felix project.
  • org.apache.felix.configadmin - we will use this package later on in the application for configuring our Time Provider service.
  • org.apache.felix.gogo.runtime - Gogo is the shell service used to manage Apache Felix (bundled with Felix).  We will use Gogo to create menu options for the OSGi container. Gogo compliant to the newest OSGi standards, and can actually be used as a replacement shell for other OSGi runtimes like Knopperfish and Eclipse Equinox. For the purpose of this tutorial, we will only use Apache Felix.
  • joda-time - the only useable Date and Time library in Java!  We will use Joda to output time from different time zones in the application.
  • junit - Unit testing library.  Please note that Maven defaults to JUnit 3, so we are changing the only dependency present in the "quickstart" archetype to the newer version of the library.

You pom.xml's dependencies element should now look like this:
<dependencies>
    <dependency>
 <groupId>org.apache.felix</groupId>
 <artifactId>org.osgi.core</artifactId>
 <version>1.0.0</version>
    </dependency>
    <dependency>
 <groupId>org.apache.felix</groupId>
 <artifactId>org.apache.felix.configadmin</artifactId>
 <version>1.2.8</version>
    </dependency>
    <dependency>
 <groupId>org.apache.felix</groupId>
 <artifactId>org.apache.felix.gogo.runtime</artifactId>
 <version>0.10.0</version>
    </dependency>
    <dependency>
 <groupId>joda-time</groupId>
 <artifactId>joda-time</artifactId>
 <version>2.0</version>
    </dependency>
    <dependency>
 <groupId>junit</groupId>
 <artifactId>junit</artifactId>
 <version>4.10</version>
 <scope>test</scope>
    </dependency>
 </dependencies>
Next, we need to add a plugin to Maven to allow it to produce OSGi bundles.  Add the following plugins section to the pom.xml's root element:
<build>
  <plugins>
   <plugin>
     <groupId>org.apache.maven.plugins</groupId>
     <artifactId>maven-compiler-plugin</artifactId>
     <version>2.3.2</version>
     <configuration>
       <target>1.5</target>
       <source>1.5</source>
     </configuration>
   </plugin>
  <plugin> 
    <groupId>org.apache.felix</groupId>
    <artifactId>maven-bundle-plugin</artifactId>
    <version>2.3.6</version>
    <extensions>true</extensions>
    <configuration>
      <instructions>
        <Export-Package>
          com.berico.timeprovider.api
        </Export-Package>
        <Private-Package>
          com.berico.timeprovider.internal.*
        </Private-Package>
        <Bundle-Activator>
          com.berico.timeprovider.internal.Activator
        </Bundle-Activator>
      </instructions>
    </configuration>
   </plugin> 
  </plugins>
</build>
The maven-bundle-plugin will provide the facilities to allow bundle create, extending Maven.  The maven-compiler-plugin adds support for annotation processing by forcing the compiler to support Java 5 (or later) language features.

For the most part, you will probably cut and paste this every time you create a bundle.  You do want to pay attention to the plugin -> configuration -> instructions element of this section.  Within this element, you will need to provide bundle-specific information.  In this case, I've specified that the com.berico.timeprovider.api package should be exported within the OSGi runtime.  This is where I will provide interfaces and model that I want consumers to work with.  Alternatively, I don't want developers to be messing with the com.berico.timeprovider.internal package (and subpackages), so I instruct the OSGi container that these are sealed.  Finally, I've specified that the com.berico.timeprovider.internal.Activator class is my bundle activator (think "main method" of the package).

The information contained within this section will be translated into the appropriate bundle metadata required by OSGi, stored in the MANIFEST.MF file.  The observant eye will notice that I did not specify the Import-Package information, required for our bundle to operate (literally having those libraries exposed by OSGi to our bundle's classloader).  One of the really cool things about the Maven plugin is the auto-generation of this metadata based on our dependency declaration in the pom.xml.  Another cool feature of the plugin is the implicit addition of the bundle's classes to the Import-Package, a convention imposed by the OSGi framework.

The last thing we need to do is modify some of the basic information included by default in the pom.xml. We are going to add a couple of properties about the bundle itself (the project.build.sourceEncoding was added by Maven already).  The bundle.symbolicName property maps directly to the Bundle-SymbolicName and the bundle.namespace (though I'm not certain) to the package namespace we want to use on Import-Package.
<properties>
  <bundle.symbolicName>com.berico.timeprovider</bundle.symbolicName>
  <bundle.namespace>com.berico.timeprovider</bundle.namespace>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
Now let's change the name of the package to reflect the bundle's symbolic name and namespace (this will appear as the Bundle's name (literally the OSGi Bundle-Name property) within Apache Felix Gogo shell:
<name>Berico Time Provider</name>
And the last change we want to make is to ensure the way Maven packages the project is in the form of a bundle and not a jar (which is the current setting):
<packaging>bundle</packaging>
With these changes, we are now ready to write some code, compile it, and package the results in a properly formatted OSGi bundle.

4.  Write an OSGi Activator.


Now that we can produce a bundle, it's time to write an Activator to demonstrate how this bundle will work in an OSGi container.  An Activator is the entry point of a bundle (it's literally a way to execute code when the bundle is started or stopped by an OSGi container).  The purpose of the Activator is to initialize your service (or tear it down); both the start and stop methods are given access to the BundleContext of the OSGi container.  Using the BundleContext, you can produce or consume services, register listeners for life cycle events with the container, among other things.

The first thing I'm going to do is delete the App class created by Maven in the com.berico.timeprovider package, and create the following classes and subpackages:
  • com.berico.timeprovider.internal.Activator - class that we are referencing as the bundle activator in our Maven pom.xml.
  • com.berico.timeprovider.api.TimeProvider - interface that we are going to offer consumers who want to request the time.
  • com.berico.timeprovider.internal.TimeProviderImpl - this is the implementation of the TimeProvider interface that we will provide consumers.
I've implemented the TimeProvider interface and TimeProviderImpl class completely, but I'm going to leave the Activator inept at the moment.  All we are going to do with the Activator is print a message to the screen when the bundle starts and stops.

TimeProvider.java
package com.berico.timeprovider.api;

public interface TimeProvider {

  String getTime();
 
  String getTime(String timezone) throws Exception;
 
  void setDefaultTimeZone(String timezone) throws Exception;
}
TimeProviderImpl.java
package com.berico.timeprovider.internal;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;

import com.berico.timeprovider.api.TimeProvider;

public class TimeProviderImpl implements TimeProvider {
  
  // Default Time Zone is Zulu/GMT (0)
  private DateTimeZone defaultTimeZone 
        = DateTimeZone.forOffsetHours(0);
  
  public String getTime() {
    
    return getTime(this.defaultTimeZone);
  }

  public String getTime(String timezone) throws Exception {
  
    return getTime(DateTimeZone.forID(timezone));
  }
  
  public void setDefaultTimeZone(String timezone) throws Exception {
    
    DateTimeZone dtz = getTimeZone(timezone);
    
    this.defaultTimeZone = dtz;
  }

  private String getTime(DateTimeZone dtz){
    
    DateTime dt = new DateTime(dtz);
    
    return dt.toString();
  }
  
  private DateTimeZone getTimeZone(String timezone) throws Exception {
    
    DateTimeZone dtz = DateTimeZone.forID(timezone);
    
    if(dtz == null){
      
      throw new Exception(
        String.format(
          "Could not parse timezone [%s].", timezone));
    }
    
    return dtz;
  }
}
Activator.java
package com.berico.timeprovider.internal;

import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;

public class Activator implements BundleActivator {

  public void start(BundleContext bundleContext) throws Exception {
    
    System.out.println("com.berico.timeprovider Started");
  }

  public void stop(BundleContext bundleContext) throws Exception {
    
    System.out.println("com.berico.timeprovider Stopped");
  }
}
We will build out the Activator in a little bit.  Before we go any further, let's create our bundle, download an OSGi container, and test to see if the bundle runs correctly.

5.  Compiling/Creating the Bundle.

Now that we have some code, let's compile the bundle.  We're going to perform these steps from the command line instead of using Eclipse.

First, let's ensure our code compiles:
mvn compile
If everything was written correctly, you should see a "BUILD SUCCESS" when Maven finishes executing the build.  Now let's produce the bundle:
mvn bundle:bundle
Once again you should see a "BUILD SUCCESS" at the end of the process.  If you go to the target directory, located in the root of the project, you should see a jar file called timeprovider-0.0.1-SNAPSHOT.jar.


I'm going to change the file extension to zip and extract the contents to show the directory structure.


Finally, let's inspect the MANIFEST.MF file to see what the Maven Bundle Plugin did:
Manifest-Version: 1.0
Bnd-LastModified: 1327273047610
Build-Jdk: 1.6.0_29
Built-By: rclayton
Bundle-Activator: com.berico.timeprovider.internal.Activator
Bundle-ManifestVersion: 2
Bundle-Name: Berico Time Provider
Bundle-SymbolicName: com.berico.time.provider
Bundle-Version: 0.0.1.SNAPSHOT
Created-By: Apache Maven Bundle Plugin
Export-Package: com.berico.timeprovider.api;version="0.0.1.SNAPSHOT"
Import-Package: com.berico.timeprovider.api;version="[0.0,1)",org.joda.t
 ime;version="[2.0,3)",org.osgi.framework;version="[1.3,2)"
Tool: Bnd-1.50.0
Awesome.  Now let's try to get this bundle to install and start in an OSGi container.

In terms of OSGi containers, you have a number of options to choose from.  The entire Eclipse platform is built on the Eclipse Equinox OSGi container.  For our purposes, I'm going to stick with Apache Felix since I am most familiar with it's environment.

Download the Apache Felix from here:  http://felix.apache.org/site/downloads.cgi.

We will also need to ensure our project's dependencies are satisfied; the org.osgi.core dependency is satisfied by the container.  The other dependencies are apart of the Apache Felix project; I will show you a nifty way to retrieve those from Felix's console.  The one library that we will have to manually retrieve is the Joda Time library.

 Download the Joda Time library from here:  http://sourceforge.net/projects/joda-time/files/joda-time/2.0/joda-time-2.0-dist.zip/download

Extract the Apache Felix archive somewhere meaningful.  Next, extract the Joda Time archive, and place the joda-time-2.0.jar in the bundle directory located in the root folder of Apache Felix.  Any OSGi bundle placed in this directory will automatically be installed by the container; for this reason, this folder is ofter referred to as the auto-deploy folder on an OSGi container.


Hey, look at that!  The org.apache.felix.gogo.runtime bundle is already there!  That's one less dependency we will need to load for this example.

Let's start Felix and install the final dependency, the org.apache.felix.configadmin bundle.

To launch Felix, you use the jar switch of the java command to launch the container.  From the root directory of the Felix installation, you would execute the following command:
java -jar bin/felix.jar
I prefer to setup an alias to this command since its so long and dependent on being in the root of that directory:
alias osgi="java -jar `pwd`/bin/felix.jar"
Now we can start the Felix container using the osgi command:


Ok, first let's look at the bundles installed within the container by calling the lb command:


Great!  We have the Joda-Time bundle installed, now we only need to get the org.apache.felix.configadmin bundle installed before we can install our newly created bundle.  Felix includes a bundle called the Apache Felix Bundle Repository that is capable of downloading commonly used OSGi bundles and automatically installing them in the current OSGi container instance.

To see the list of available bundles that can be downloaded by the Felix Bundle Repository, simply type obr:list in the Gogo shell:
g! obr:list
And you should see...
Apache Felix Bundle Repository (1.6.6, ...)
Apache Felix Configuration Admin Service (1.2.4, ...)
Apache Felix Declarative Services (1.6.0, ...)
Apache Felix EventAdmin (1.0.0)
Apache Felix File Install (3.0.2, ...)
Apache Felix Gogo Command (0.10.0, ...)
Apache Felix Gogo Runtime (0.10.0, ...)
Apache Felix Gogo Shell (0.10.0, ...)
Apache Felix Gogo Shell Commands (0.2.0)
Apache Felix Gogo Shell Console (0.2.0)
Apache Felix Gogo Shell Launcher (0.2.0)
Apache Felix Gogo Shell Runtime (0.2.0)
Apache Felix Http Api (2.0.4)
Apache Felix Http Base (2.0.4)
Apache Felix Http Bridge (2.0.4)
Apache Felix Http Bundle (2.0.4)
Apache Felix Http Jetty (2.0.4)
Apache Felix Http Proxy (2.0.4)
Apache Felix Http Samples - Filter (2.0.4)
Apache Felix Http Samples - Whiteboard (2.0.4)
Apache Felix HTTP Service Jetty (1.0.1, ...)
Apache Felix Http Whiteboard (2.0.4)
Apache Felix iPOJO (1.8.0, ...)
Apache Felix iPOJO (0.8.0)
Apache Felix iPOJO API (1.6.0, ...)
Apache Felix iPOJO Arch Command (1.6.0, ...)
Apache Felix iPOJO Composite (1.8.0, ...)
Apache Felix iPOJO Composite (1.0.0, ...)
Apache Felix iPOJO Event Admin Handler (1.8.0, ...)
Apache Felix iPOJO Extender Pattern Handler (1.4.0, ...)
Apache Felix iPOJO Extender Pattern Handler (1.0.0, ...)
Apache Felix iPOJO Gogo Command (1.0.1, ...)
Apache Felix iPOJO JMX Handler (1.4.0, ...)
Apache Felix iPOJO Temporal Service Dependency Handler (1.6.0, ...)
Apache Felix iPOJO URL Handler (1.6.0, ...)
Apache Felix iPOJO WebConsole Plugins (1.6.0, ...)
Apache Felix iPOJO White Board Pattern Handler (1.2.0, ...)
Apache Felix iPOJO White Board Pattern Handler (1.6.0, ...)
Apache Felix Log Service (1.0.0)
Apache Felix Metatype Service (1.0.2, ...)
Apache Felix Prefrences Service (1.0.2)
Apache Felix Remote Shell (1.0.4, ...)
Apache Felix Remote Shell (1.1.2, ...)
Apache Felix Shell Service (1.4.2, ...)
Apache Felix Shell TUI (1.4.1, ...)
Apache Felix UPnP Base Driver (0.8.0)
Apache Felix UPnP Extra (0.4.0)
Apache Felix UPnP Tester (0.4.0)
Apache Felix Web Console Event Plugin (1.0.2)
Apache Felix Web Console Memory Usage Plugin (1.0.0)
Apache Felix Web Console Memory Usage Plugin (1.0.2)
Apache Felix Web Console UPnP Plugin (1.0.0)
Apache Felix Web Management Console (3.1.2, ...)
Apache Felix Web Management Console (3.1.2, ...)
OSGi OBR Service API (1.0.0)
OSGi R4 Compendium Bundle (4.0.0)
Servlet 2.1 API (1.0.0)
The Apache Felix Configuration Admin Service is the final dependency we need to install.  We can install the dependency using the obr:deploy command, supply the argument org.apache.felix.configadmin as the bundle we want deployed:
g! obr:deploy org.apache.felix.configadmin
Which displays...
Target resource(s):
-------------------
   Apache Felix Configuration Admin Service (1.2.4)

Optional resource(s):
---------------------
   Apache Felix Log Service (1.0.0)

Deploying...
done.
Very nice.  Remember, you can check to see if the bundle is installed by using the lb command (lb = "list bundles").  Now let's stop the container.  At the time of this post, I have not yet found an elegant way to terminate the container.  Felix use to have a command called "shutdown", but I cannot find it anymore.  One can always press control-c if the process is running in a terminal, but I found a more elegant (but ugly) way.  If you execute lb, you will see that the first bundle (0, or zero) is the System Bundle.  All other bundles require the system bundle.  So, if you tell the framework to stop the System Bundle, it will shutdown the container and all other bundles dependent on it:
g! stop 0
Your container should yield control back to BASH (or DOS for the poor Windows users).

Even though people will tell you using the auto-deploy feature is not the best way to install a bundle, let's live dangerously and drop our timeprovider bundle into Felix's bundle directory.  Your bundle directory should look like this now:


Let's start Felix up again and see what happens...
osgi
Results in...
com.berico.timeprovider Started
____________________________
Welcome to Apache Felix Gogo

g! 
Wicked! As you saw in the Activator class, when the bundle started (literally the start method was called), we printed the following message out to the console: com.berico.timeprovider Started.

So, if the bundle started successfully, and we saw our start message, we should see the stop message when we tell the container to stop the bundle.  Let's try it out.

Find the id of the bundle using the lb command:
g! lb
START LEVEL 1
   ID|State      |Level|Name
    0|Active     |    0|System Bundle (4.0.2)
    1|Active     |    1|Joda-Time (2.0.0)
    2|Active     |    1|Apache Felix Bundle Repository (1.6.6)
    3|Active     |    1|Apache Felix Gogo Command (0.12.0)
    4|Active     |    1|Apache Felix Gogo Runtime (0.10.0)
    5|Active     |    1|Apache Felix Gogo Shell (0.10.0)
    6|Installed  |    1|Apache Felix Configuration Admin Service (1.2.4)
    7|Installed  |    1|Apache Felix Log Service (1.0.0)
    8|Active     |    1|Berico Time Provider (0.0.1.SNAPSHOT)
Now tell the container to stop the bundle supplying the timeprovider's bundle id (in this case, number 8):
g! stop 8
com.berico.timeprovider Stopped
Viola! Our stop message is displayed as expected.

If you gotten this far (assuming you are doing this self and not cheating by simply reading this post), congratulate yourself.  You've created an OSGi bundle, using Maven, and deployed it to an OSGi container.  Furthermore, you've done some cool stuff at the Gogo command line, like installing a bundle using the Felix Bundle Repository.

6.  Registering a Service in OSGi.

Now it's time to start playing with services, the core reason you probably want to learn OSGi.  We created our TimeProvider interface for a reason!  We want to expose this interface as an OSGi service that another bundle can consume.  So how do we do this?  Well, it actually quite easy as you will see.

The only class that needs to change in order to provide an instance of TimeProvider as a service for other bundles is the Activator class.  These changes are reflected below:
package com.berico.timeprovider.internal;

import java.util.Dictionary;
import java.util.Hashtable;

import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;

import com.berico.timeprovider.api.TimeProvider;

public class Activator implements BundleActivator {
  
  //Save a handle to the service we registered
  private static 
     ServiceRegistration timeProviderRegistration = null;
  
  public void start(BundleContext bundleContext) throws Exception {
    
    TimeProvider timeProvider = new TimeProviderImpl();
    
    // Ignore this ugliness
    Dictionary properties = new Hashtable();
    
    timeProviderRegistration = 
      bundleContext.registerService(
        TimeProvider.class.getName(), timeProvider, properties);  
    
    System.out.println("com.berico.timeprovider Started");
  }

  public void stop(BundleContext bundleContext) throws Exception {
    
    if(timeProviderRegistration != null){
      
      bundleContext.ungetService(
        timeProviderRegistration.getReference());
    }
    
    System.out.println("com.berico.timeprovider Stopped");
  }
}
A couple of things to note about the changes we've made to the Activator.  We interact with the OSGi environment through the BundleContext.  This includes registering and unregistering services.  To register a service, you simply need to call the registerService method on the BundleContext.  The method takes three arguments.  The first is the String name of the service.  The second parameter is the service itself (implementation).  Finally, we supply metadata to the OSGi framework about the service we are registering.  This metadata is important for certain aspects of OSGi, including bundle queries, where you can search for bundles by the metadata they provide.  In our case, we have nothing useful to supply, so we give it an empty (but initialized) Dictionary collection.

If you've done any serious work in Java, you are probably wondering why we are not using Java Generics.  The OSGi framework was originally created to support mobile phone applications before Generics were introduced on the Java platform.  Since the specification targets platforms pre-Java 5, we are stuck "boxing" and "unboxing" objects and using non-generic collections.  From what I understand, the newest specification for OSGi will include an API supporting generics, cleaning up this ugliness.

Now that we've learned how to register a service with the OSGi container, let's create a new bundle that will consume the service.

7.  Consuming a Service in OSGi.

Without going through the steps all over again, I'm going to create a new, separate bundle called timeconsumer.  In the new timeconsumer project, I'm only going to create one BundleActivator class (and nothing else), which I will unimaginatively call Activator:
package com.berico.timeconsumer;

import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;

import com.berico.timeprovider.api.TimeProvider;

public class Activator implements BundleActivator {

  private static ServiceReference timeProviderReference = null;
  
  public void start(BundleContext bundleContext) throws Exception {
    
    timeProviderReference
      = bundleContext.getServiceReference(
        TimeProvider.class.getName());
    
    if(timeProviderReference != null){
    
      TimeProvider timeProvider = 
        (TimeProvider)bundleContext.getService(timeProviderReference);
      
      System.out.println("com.berico.timeconsumer Started");

      System.out.println(
        String.format("Current Time is %s", timeProvider.getTime()));
    }
    else {
      
      System.out.println("Could not find a valid Time Provider");
    }
  }

  public void stop(BundleContext bundleContext) throws Exception {
    
    if(timeProviderReference != null){
      
      bundleContext.ungetService(timeProviderReference);
    }
    
    System.out.println("com.berico.timeconsumer Stopped");
  }

}
The Activator class is first grabbing a ServiceReference to the service we want to consume.  The ServiceReference contains the service id along with information about the bundle the service was registered from.  We next ensure the reference is not null.  If it is null, we print a message to the console saying we couldn't find the time provider.  If the reference is set, we go back to the BundleContext grabbing the service.  Remember that OSGi doesn't currently support generics, so we have to cast the object that is returned to the appropriate type.  Finally, we print the time to the console, provided by the time provider instance.

The hard part, believe it or not, is the pom.xml and not the Activator class.  There are some little tricks we have to do in order to get our timeconsumer to use the timeproducer as a dependency.  The first thing we need to do is install our timeproducer as an artifact in the local Maven repository.  This is a simple command that you can execute within the root directory of the timeproducer project:
mvn install:install
With the timeproducer bundle installed, we need to add a dependency to the timeproducer project in our timeconsumer's pom.xml:
<dependency>
  <groupId>com.berico.time</groupId>
  <artifactId>timeprovider</artifactId>
  <version>0.0.1-SNAPSHOT</version>
</dependency>
Adding the dependency is only the first step.  Since we depend on the timeproducer to start and register the TimeProvider implementation with the OSGi container before we attempt to consume the service in the timeconsumer bundle, we will need to add a special element to the plugin > configuration > instructions of the Maven Bundle Plugin configuration:
<Require-Bundle>com.berico.time.provider</Require-Bundle>
The Require-Bundle attribute essentially tells the OSGi container not to start the bundle until the bundles referenced in the value of the property are started.  The strange thing you might notice is the name of the bundle: com.berico.time.provider.  The groupId in our timeprovider Maven configuration is com.berico.time and our name is timeprovider.  The bundle.symbolicName property in the pom.xml is com.berico.timeprovider.  However, when the bundle is created by Maven, the MANIFEST.MF is written with the Bundle-SymbolicName as com.berico.time.provider.  I suspect there is an error with Maven Bundle Plugin causing it to improperly output the symbolic name.  If you are ever unsure what to use, the simple way to find out the symbolic name of a bundle is to unzip the bundle and open the MANIFEST.MF in a text editor.  If the bundle in question will install in the OSGi container, you can also execute the headers command of the Gogo shell, followed by the id of the bundle to inspect:
g! headers 19
Which Produces:
Berico Time Provider (19)
-------------------------
Bnd-LastModified = 1327295763667
Build-Jdk = 1.6.0_29
Built-By = rclayton
Bundle-Activator = com.berico.timeprovider.internal.Activator
Bundle-ManifestVersion = 2
Bundle-Name = Berico Time Provider
Bundle-SymbolicName = com.berico.time.provider
Bundle-Version = 0.0.1.SNAPSHOT
Created-By = Apache Maven Bundle Plugin
Export-Package = com.berico.timeprovider.api;version="0.0.1.SNAPSHOT"
Import-Package = com.berico.timeprovider.api;version="[0.0,1)",org.joda.time;version="[2.0,3)",org.osgi.framework;version="[1.3,2)"
Manifest-Version = 1.0
Tool = Bnd-1.50.0
g! 
Now let's drop the timeconsumer bundle in the auto-deploy directory of Felix.
Start up the OSGi container:
osgi
And you should see...
com.berico.timeprovider Started
Current Time: 2012-01-24T02:48:58.374Z
com.berico.timeconsumer Started
Current Time is 2012-01-24T02:48:58.395Z
____________________________
Welcome to Apache Felix Gogo

g! 
Success!  We have now not only produced a service in OSGi, but also consumed one.  Next, we will learn how to use the Configuration Admin specification of OSGi to dynamically receive configuration updates.

8.  Consuming Configuration Updates.

Once you've learned how to create bundles and register and consume services, I find the next thing most engineers want to learn how to do is pass configuration to there services.  Who can argue with that?  I mean, what's the point of creating highly modular systems if you hard code the configuration those module rely on?

There are a number of ways to configure an OSGi application; here are a couple:

Embed your settings in a property file.

This is somewhat idiomatic for Java.  I mean, it's incredibly simple to call System.getProperty to pull the desired configuration value from the environment.  The Felix container even offers a global properties file you can use to bundle all of this configuration data (conf/config.properties).  In many cases this works if you know what your configuration is going to be before you start the container.

Create and register a configuration service with the OSGi container.

Another option is to create a bundle and some sort of API around configuration.  For instance, you could register a Config object with the container, which is consumed by the service/bundle you need configured.  This adds an interesting benefit because you could prevent a the bundle from activating unless the Config object becomes available.  You can also change the configuration by recompiling the bundle with the Config object and updating the bundle, which should in turn update the dependent service.

Use the OSGi Specification for Configuration Admin.

The Configuration Admin specification for OSGi formalizes the previous notion (services dedicated for configuration) providing a nice model for handling these configuration updates, along with a persistence mechanism that ensure updates remain once they are set.  More importantly, the Configuration Admin service provides hooks for allowing configuration updates without needing to create special configuration bundles.  A service that wants to use the Configuration Admin service merely needs to implement the ManagedService interface.  This interface is made available to us from the org.apache.felix.configadmin artifact we referenced as a dependency in our Maven pom.xml file earlier in the post.

I've taken the liberty to implement the ManagedService interface and its corresponding update method:
package com.berico.timeprovider.internal;

import java.util.Dictionary;
import java.util.Hashtable;

import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;

import com.berico.timeprovider.api.TimeProvider;

public class Activator implements BundleActivator, ManagedService {

  private static ServiceRegistration timeProviderRegistration = null;
  private static ServiceRegistration configRegistration = null;
  private static TimeProvider timeProvider = new TimeProviderImpl();
  
  public void start(BundleContext bundleContext) throws Exception {
    
    // Properties for the TimeProvider registration
    Dictionary registrationProperties = new Hashtable();
    
    timeProviderRegistration = 
      bundleContext.registerService(
        TimeProvider.class.getName(), 
        timeProvider, 
        registrationProperties);  
    
    //Properties for the Config Admin Registration
    Dictionary configProperties = new Hashtable();
    configProperties.put(Constants.SERVICE_PID, "timeprovider.pid");
    
    configRegistration = 
      bundleContext.registerService(
        ManagedService.class.getName(), 
        this, configProperties);
    
    System.out.println("com.berico.timeprovider Started");
    
    printTime();
  }

  public void stop(BundleContext bundleContext) throws Exception {
    
    unregister(timeProviderRegistration, bundleContext);
    
    unregister(configRegistration, bundleContext);  
    
    System.out.println("com.berico.timeprovider Stopped");
  }
  
  public void updated(Dictionary configuration) throws ConfigurationException {
    
    System.out.println("Updating Time Provider Configuration.");
    
    if(configuration != null){
      
      String timezone = configuration.get("timezone").toString();
      
      System.out.println(
          String.format("Changing Time Zone to: %s", timezone));
      
      try {
        
        timeProvider.setDefaultTimeZone(timezone);
      
        printTime();
        
      } catch (Exception e) {
        
        System.out.println(
          String.format(
            "Could not set the provided timezone [%s]; reason: %s", 
            timezone, 
            e.getMessage()));
      }
      
    }
    else {
      
      System.out.println("Time Service: Configuration Null");
    }
  }
  
  private static void unregister(
      ServiceRegistration registration, BundleContext bundleContext){
    
    if(registration != null){
      
      bundleContext.ungetService(registration.getReference());
    }
  }
  
  private static void printTime(){
    
    System.out.println(
      String.format("Current Time: %s", timeProvider.getTime()));
  }
  
}
Now that you've see how we can consume configuration updates, I'm going to demonstrate how to produce an update.  Instead of creating a whole new bundle to make this happen, I'm going to show how to extend the Apache Felix Gogo Shell to serve as a mechanism for updating our timeproducer service.

9.  Writing Menu Extensions for the Apache Felix Gogo shell.

Writing Shell Extensions for the Felix Gogo shell is surprising easy.  Shell commands are actually methods on a POJO, and Gogo does some clever Reflection work to turn those commands into executable actions.

Let's create a simple menu extension that prints the time on command, as well as, sets the default timezone:
package com.berico.timeprovider.internal;

import java.io.IOException;
import java.util.Dictionary;
import java.util.Hashtable;

import org.apache.felix.service.command.Descriptor;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;

import com.berico.timeprovider.api.TimeProvider;

public class ShellCommands {

  private final BundleContext bundleContext;
  private ConfigurationAdmin configAdmin;
  
  ShellCommands(BundleContext bundleContext){
    
    this.bundleContext = bundleContext;
    
    getConfigAdmin();
  }
  
  @Descriptor("Show the current time.")
  public void show(){
    
    ServiceReference timeProviderReference = 
      bundleContext.getServiceReference(
        TimeProvider.class.getName());
    
    TimeProvider timeProvider = 
      (TimeProvider)bundleContext.getService(
        timeProviderReference);
    
    System.out.println(timeProvider.getTime());
  }
  
  @Descriptor("Set the Default Time Zone for the Time Provider")
  public void timezone(
      @Descriptor("Valid Time Zone") 
        String timezone){
    
    System.out.println(
      String.format("Setting Time Zone: %s", timezone));
    
    ServiceReference configurationAdminReference = 
            bundleContext.getServiceReference(
              ConfigurationAdmin.class.getName());  
    
    Configuration config = null;
    
    try {
    
      config = configAdmin.getConfiguration(
              "timeprovider.pid");
      
      Dictionary props = config.getProperties();
      
      if (props == null) {
          props = new Hashtable();
      }

      props.put("timezone", timezone);

      config.update(props);
      
    } catch (IOException e) {
      
      e.printStackTrace();
    }
    
  }
  
  private void getConfigAdmin(){
    
    if(this.configAdmin == null){
    
      ServiceReference configurationAdminReference = 
              this.bundleContext.getServiceReference(
                ConfigurationAdmin.class.getName()); 
    
      if (configurationAdminReference != null) 
          {  
        
              this.configAdmin 
                = (ConfigurationAdmin) 
                  bundleContext.getService(
                    configurationAdminReference);  
          }
    }
  }
}
In this example, the methods show and timezone will serve as the "executables" the OSGi containers call when a command is typed in the Gogo shell.  In the show method, we grab an instance of the TimeProvider instance from the OSGi context and print the current time to the shell.  In the timezone method, instead of retrieving the service directly from the OSGi context, we instead grab a reference to the ConfigurationAdmin service, create a new set of properties for the timeprovider service (we refer to the service using the "pid" of the timeprovider, what I've simply called "timeprovider.pid"), and tell the ConfigurationAdmin service to update the properties for that service.  This will call the update method on our Activator class, which will in turn set the default time zone.

The last thing to mention is the @Descriptor annotation.  This is used by the Gogo shell to provide contextual help for the command.  If the annotation is placed over the method name, the description applies to the command.  If the annotation is on a function argument, it applies to one of the command's parameters.

In order to demonstrate the difference in time zone when we make changes to the default time zone stored within the TimeProvider, I'm going to make a small modification to the TimeProviderImpl.getTime method:
private String getTime(DateTimeZone dtz){
  
  DateTime dt = DateTime.now(dtz);
  
  return dt.toString("ZZZ - yyyy-MM-dd HH:mm:ss");
}
We register the ShellCommands class like any other service in OSGi.  You might be wondering, how does the Felix Gogo Shell know to "pick up" the service after it's registered with the container?  This is another example of when the properties (metadata) provided to the container is useful for service consumption.  Let's modify timeprovider's Activator class to register the ShellCommands service with the container:
//Properties for the Gogo Shell
Dictionary shellCommandsProperties = new Hashtable();
    
shellCommandsProperties.put("osgi.command.scope", "time");
    
shellCommandsProperties.put("osgi.command.function", 
  new String[] {"show", "timezone"});
    
bundleContext.registerService(
  ShellCommands.class.getName(), 
  new ShellCommands(bundleContext), 
  shellCommandsProperties);
Adding the osgi.command.scope and osgi.command.function properties to the service registration will provide the appropriate information to the Felix Gogo plugin to detect and install the ShellCommands instance as a viable addition to the menu.  Using some nifty Reflection, the Felix Gogo service can pull the description of the commands and their parameters from the annotations.  Reading the signature of the method, the Felix Gogo service can determine the number and types of parameters required to execute the function, mapping those parameters onto the particular method instance.

10.  Dynamically configuring a Service using the Gogo shell.

Now we are ready to demonstrate how to dynamically configure a service using Gogo shell.  Before we go any further, it would be wise to uninstall the timeprovider and timeconsumer bundles from the OSGi container, recompile and package them, and place the new versions in the auto-deploy directory of Apache Felix.
Updating Time Provider Configuration.
Time Service: Configuration Null
com.berico.timeprovider Started
Current Time: UTC - 2012-01-24 23:07:37
com.berico.timeconsumer Started
Current Time is UTC - 2012-01-24 23:07:37
____________________________
Welcome to Apache Felix Gogo

g!
Let's first start by seeing if our commands were loaded into the Gogo shell.  We can see a list of commands by typing the "help" command:
g! help
The Felix Gogo Shell will dump a list of commands:
felix:bundlelevel
felix:cd
felix:frameworklevel
felix:headers
felix:help
felix:inspect
felix:install
felix:lb
felix:log
felix:ls
felix:refresh
felix:resolve
felix:start
felix:stop
felix:uninstall
felix:update
felix:which
gogo:cat
gogo:each
gogo:echo
gogo:format
gogo:getopt
gogo:gosh
gogo:grep
gogo:not
gogo:set
gogo:sh
gogo:source
gogo:tac
gogo:telnetd
gogo:type
gogo:until
obr:deploy
obr:info
obr:javadoc
obr:list
obr:repos
obr:source
time:show
time:timezone
g! 
At the bottom you will notice our two commands prefixed by time.  If you recall, time was the osgi.command.scope (think namespace).  Our two methods, registered as an array of Strings with the osgi.command.function key, appear after the scope.

We can inspect the contextual help message of any command by calling help again and supplying the name of the command:
g! help show
Which displays:
show - Show the current time.
   scope: time
And the help message for the timezone command:
g! help timezone
Which displays:
timezone - Set the Default Time Zone for the Time Provider
   scope: time
   parameters:
      String   Valid Time Zone
Keep in mind that you can always prefix a command with it's scope (e.g.: time:timezone; if there isn't another command with the same name, you can remain lazy like me and simply use the command name.

Let's execute the timezone command supplying America/Phoenix (a Joda Time constant) for our new timezone:
g! timezone America/Phoenix
We get a response first from inside our ShellCommands.timezone method notifying us that the timezone is being set, followed by notifications within the Activator.update method indicating it has received new configuration:
Setting Time Zone: America/Phoenix
Updating Time Provider Configuration.
Changing Time Zone to: America/Phoenix
Current Time: America/Phoenix - 2012-01-24 16:11:37
We also have the option to display the time at any time by calling the show command:
g! time:show
Which displays:
America/Phoenix - 2012-01-24 17:49:03

Conclusion


Although this was a very long post, my intent was to provide a complete introduction into creating an OSGi bundle in Maven, registering and consuming services, using the ConfigurationAdmin service, and manipulating the Apache Felix Gogo shell.  In this tutorial, I didn't spend a whole lot of time talking about the theory and mechanics of OSGi.  Thanks for your time and patience!

For an in depth look at OSGi, I recommend the following books:

OSGi in Depth
http://www.manning.com/alves/
OSGi in Action
http://www.manning.com/hall/



No comments:

Post a Comment

Note: Only a member of this blog may post a comment.