Dynamic Service Configuration

OSGi provides a standard configuration mechanism called Config Admin. This allows the location of configuration information to be decoupled from the code that requires the configuration. Configuration is passed through to services via a Map or Hashtable, and they can then configure themselves appropriately.

As with other parts in OSGi, this can also be dynamically updated. When the configuration source changes, an event can flow through to the service or component in order to allow it to reconfigure itself.

Installing Felix FileInstall

Config Admin itself is an OSGi service, and it may be supplied by different configuration agents. A de facto standard is Apache Felix's FileInstall, which can also be used to install bundles into an OSGi runtime.

FileInstall is available from the Apache Felix site at http://felix.apache.org as well as Maven Central. Download org.apache.felix.fileinstall-3.2.8.jar and import it into Eclipse as a plug-in project by navigating to File | Import | Plug-in Development | Plug-ins and Fragments to enable it to run at test runtime.

To use FileInstall, a system property felix.fileinstall.dir must be specified. It defaults to ./load from the current working directory, but for the purposes of testing, this can be specified by adding a VM argument in the launch configuration that appends -Dfelix.fileinstall.dir=/tmp/config or some other location. This can be used to test modifications to the configuration later.

Tip

Make sure that FileInstall is configured to start when runtime begins, so that it picks up configurations. This can be done by specifying the start level on the OSGi framework launch configuration page.

Installing Config Admin

To configure services, Config Admin needs to be installed into the runtime as well. The two standard implementations of these are Felix Config Admin and Equinox Config Admin. The latter does not come with Eclipse by default, and the Felix version is available from Maven Central and should be preferred. Download org.apache.felix.configadmin-1.8.0.jar from Maven Central or from the book's GitHub repository.

Import this as a plug-in project to Eclipse by navigating to File | Import | Plug-in Development | Plug-ins and Fragments so that it can be used as a bundle in the OSGi framework.

Configuring Declarative Services

A component created by Declarative Services can have configuration passed in a Map. A component can have an activate method, which is called after the component's dependencies have become available (along with a corresponding deactivate method). There is also a modified method that can be used to respond to changes in the configuration without stopping and restarting the component.

To configure AtomFeedParser with Config Admin, add a configure method that takes a Map of values. If it's not null, and there is a key max, then parse it as int and use that as the max value, as shown in the following code:

private int max = Integer.MAX_VALUE;
public void configure(Map<String, Object> properties) {
  max = Integer.MAX_VALUE;
  if (properties != null) {
    String maxStr = (String) properties.get("max");
    if (maxStr != null) {
      max = Integer.parseInt(maxStr);
    }
  }
}

To ensure that the method gets called, modify the service component document to add the activate="configure" and modified="configure" attributes:

<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
  modified="configure" activate="configure"
  name="AtomFeedParser">

Finally, create a properties file called AtomFeedParser.cfg with the content max=1, and place it in the location of felix.fileinstall.dir.

Now when the application is run, the configuration should be loaded and should configure AtomFeedParser, such that when a feed is added, it shows a maximum of one value. Modify the configuration file and refresh the feeds during the Eclipse runtime, and it should pick up the new value.

Tip

If nothing is seen, verify that felix.fileinstall.dir is specified correctly using props | grep felix from the OSGi console. Also, verify that the Felix fileinstall and configadmin bundles have started. Finally, verify that the methods in the component are public void and are defined correctly in the component config.

Config Admin outside of DS

It is possible to use Config Admin outside of the Declarative Services specification. Services can be configured directly using the ManagedService interface of the Config Admin specification.

When a service requires configuration, it is registered under the ManagedService interface in the registry with an associated Persistent ID (PID). Config Admin will notice the service being published and use that to feed updated configuration information through the updated method. Since the specification has been with OSGi for some time, it uses a Dictionary to specify property values instead of a Map.

This technique can be used to acquire configuration information for a singleton, such as a BundleActivator. If the activator registers itself as a ManagedService interface, then it will receive configuration updates through Config Admin. For example, to aid in testing the user interface, the mock feed can be enabled through a debug mode in the Activator class in the feeds.ui plug-in.

Modify the META-INF/MANIFEST.MF file of the com.packtpub.e4.advanced.feeds.ui plug-in to include org.osgi.service.cm as an imported package, and add the ManagedService interface to the Activator class. In the start method, register an instance of itself as the managed service:

public void start(BundleContext context) throws Exception {
  super.start(context);
  plugin = this;
  Dictionary<String, String> properties = 
   new Hashtable<String, String>();
  properties.put(Constants.SERVICE_PID,
   "com.packtpub.e4.advanced.feeds.ui");
  context.registerService(ManagedService.class, this, properties);
}

In the implementation of the updated method, detect whether the configuration is passed in an element debug with the value true. If so, set a boolean debug field to be true. Expose this via an accessor method isDebug. Having a print method can enable debugging and verify that the configuration changes are being applied for testing purposes:

private boolean debug;
public boolean isDebug() {
  return debug;
}
public void updated(Dictionary<String, ?> configuration)
 throws ConfigurationException {
  debug = configuration != null
  && "true".equals(configuration.get("debug"));
  if (debug) {
    System.out.println("Debugging enabled");
  } else {
    System.out.println("Debugging disabled");
  }
}

Finally, to see the mentioned behavior in action, modify the FeedContentProvider class to skip class implementation names when the service name implementation does not contain the string "Mock" and is in the debug mode:

public Object[] getChildren(Object parentElement) {
  …
  if (parentElement instanceof Feed) {
    Feed feed = (Feed)parentElement;
    FeedParserFactory factory = FeedParserFactory.getDefault();
    List<IFeedParser> parsers = factory.getFeedParsers();
    for (IFeedParser parser : parsers) {
      if(Activator.getDefault().isDebug()) {
        if(!parser.getClass().getName().contains("Mock")) {
          continue;
        }
      }
    }
  }
  …
}

Now run the test application, create a new feed, and point it at a valid Atom or RSS feed. When the bookmarks project is refreshed, the real entries will be seen. To enable the debug mode, create a configuration properties file at /tmp/config/com.packtpub.e4.advanced.feeds.ui.cfg with the content debug=true. Provided that DS is working correctly, it should display Debugging enabled in the console window, and subsequent refreshing of the feed should show the mock entries being created instead.

Services and ManagedService

It is possible to register services that implement both a service (such as AtomFeedParser) and the ManagedService interface (to acquire configuration). However, the problem comes when the service is registered without any configuration data. Should the service be valid if there is no configuration data present? Or will the defaults work as expected? If there must be configuration data, how will the service be prevented from being called until the configuration data is available?

The best way is to rely on something like Declarative Services to do the right thing (and with less work), but if for whatever reason it needs to be implemented then this can be done as well.

If the service can run without explicit configuration data, then the service can be registered both as its service interface (such as IFeedParser) as well as the ManagedService interface. The two interfaces will be registered at the same time, and hence, clients may call the service before the configuration is available.

If the service needs to have configuration data before being made available, then the service can be registered under the ManagedService interface only. When the updated method is called with configuration data, it can call the registerService method to register itself under the service interface. When the updated method is called with no data, it can unregister the service.

Since this is a case where the full life cycle of the service is used, it is appropriate to use a ServiceRegistration instance to keep a handle on the configured service.

Note

Note that the ServiceRegistration objects are not supposed to be shared between bundles, but only used by the bundle that registered them.

Creating an EmptyFeedParser class

Create a new feed parser, EmptyFeedParser, but place it in the UI bundle (since it needs an Activator class or some other start-up process to register it as a service, it needs to be hooked onto the UI package for ease of testing). The EmptyFeedParser class should implement both IFeedService and ManagedService:

public class EmptyFeedParser implements 
 IFeedParser, ManagedService {
  public void updated(Dictionary<String, ?> properties)
   throws ConfigurationException {
  }
  public List<FeedItem> parseFeed(Feed feed) {
    return new ArrayList<FeedItem>(0);
  }
}

To register this, add a line in the start method of the Activator class that registers this as a ManagedService interface. It should use the PID com.packtpub.e4.advanced.feeds.ui.EmptyFeedParser, to allow for easy configuration testing:

public void start(BundleContext context) throws Exception {
  … 
  Dictionary<String, String> properties
   = new Hashtable<String, String>();
  properties.put(Constants.SERVICE_PID,
   EmptyFeedParser.class.getName());
  context.registerService(ManagedService.class,
   new EmptyFeedParser(), properties);
}

The final piece is to implement the updated method so that it registers the service when a configuration is provided, and remove it when it is no longer needed. If the configuration is not null, then a new service can be registered and the resulting registration object stored in an instance variable. If the configuration is null, then the registration object should be removed, if it existed. The implementation will look like the following:

private ServiceRegistration<IFeedParser> registration;
public void updated(Dictionary<String, ?> properties)
 throws ConfigurationException {
  BundleContext context = FrameworkUtil
   .getBundle(EmptyFeedParser.class)
   .getBundleContext();
  if (properties != null) {
    if (registration == null) {
      System.out.println(
       "Registering EmptyFeedParser for the first time");
      registration = context.registerService(
       IFeedParser.class, this, properties);
    } else {
      System.out.println("Reconfiguring EmptyFeedParser");
      registration.setProperties(properties);
    }
  } else {
    if (registration != null) {
      System.out.println("Deconfiguring EmptyFeedParser");
      registration.unregister();
    }
    registration = null;
  }
}

Note

This shows the use of a ServiceRegistration object that permits both service reconfiguration (via the setProperties method) as well as unregistration via the unregister method. This can be used independently of Config Admin.

Configuring the EmptyFeedParser

Running the application followed by creating a configuration properties file at /tmp/config/com.packtpub.e4.advanced.feeds.ui.EmptyFeedParser.cfg should result in the output being seen in the console that the service is being created. Similarly, changes to the file will show the updated messages being displayed, and when the file is removed, the service will go away.

Tip

Felix has a default timeout of two seconds to scan for changes, which can be configured to a different value via the felix.fileinstall.poll property, which takes a number in milliseconds between polls.

First, find out what bundle.id is being used for the feeds.ui bundle:

osgi> ss | grep feeds.ui
27 ACTIVE com.packtpub.e4.advanced.feeds.ui_1.0.0.qualifier

Now the bundle identifier is found (27 in this case), it can be used to ensure that it is started and investigate what services are provided:

osgi> start 27
osgi> bundle 27 | grep service.pid
{org.osgi.service.cm.ManagedService}={service.id=294,
 service.pid=com.packtpub.e4.advanced.feeds.ui}
{org.osgi.service.cm.ManagedService}={service.id=295,
 service.pid=com.packtpub.e4.advanced.feeds.ui.EmptyFeedParser}

The two managed services are the ones created earlier in this chapter; the one that controls debugging and the one that controls the EmptyFeedParser class just created.

Now create an empty file in the configuration directory (the value specified by the felix.fileinstall.dir property) /tmp/config/com.packtpub.e4.advanced.feeds.ui.EmptyFeedParser.cfg. The following can be seen in the console:

Registering EmptyFeedParser for the first time
osgi> bundle 27 | grep service.pid
{org.osgi.service.cm.ManagedService}={service.id=294,
 service.pid=com.packtpub.e4.advanced.feeds.ui}
{org.osgi.service.cm.ManagedService}={service.id=295 ,
 service.pid=com.packtpub.e4.advanced.feeds.ui.EmptyFeedParser}
{com.packtpub.e4.advanced.feeds.IFeedParser}={service.id=296,
 service.pid=com.packtpub.e4.advanced.feeds.ui.EmptyFeedParser,
 felix.fileinstall.filename=file:/tmp/config/
 com.packtpub.e4.advanced.feeds.ui.EmptyFeedParser.cfg}

The creation of the external configuration file has resulted in the service being registered automatically. Now add a line, a=banana, in the EmptyFeedParser.cfg file and see what happens:

Reconfiguring EmptyFeedParser
osgi> bundle 27 | grep service.pid
{org.osgi.service.cm.ManagedService}={service.id=294,
 service.pid=com.packtpub.e4.advanced.feeds.ui}
{org.osgi.service.cm.ManagedService}={service.id=295,
 service.pid=com.packtpub.e4.advanced.feeds.ui.EmptyFeedParser}
{com.packtpub.e4.advanced.feeds.IFeedParser}={service.id=296,
 service.pid=com.packtpub.e4.advanced.feeds.ui.EmptyFeedParser,
 a=banana, felix.fileinstall.filename=file:/tmp/config/
 com.packtpub.e4.advanced.feeds.ui.EmptyFeedParser.cfg}

In addition to the service being reconfigured (with the reconfiguring debug message shown), the additional service property a=banana has been added to the list.

Note

Because the service method registration.setProperties was used, the same service stays bound. An alternative strategy is to unregister the service and register a new one. Doing so will require clients to rebind themselves to the new service, so if this can be avoided, it makes the clients easier to reason about.

Finally, remove the configuration file and see what happens:

Deconfiguring EmptyFeedParser
osgi> bundle 27 | grep service.pid
{org.osgi.service.cm.ManagedService}={service.id=294,
 service.pid=com.packtpub.e4.advanced.feeds.ui}
{org.osgi.service.cm.ManagedService}={service.id=295,
 service.pid=com.packtpub.e4.advanced.feeds.ui.EmptyFeedParser}

Service factories

A service factory can be used to create services on demand, rather than being provided up front. OSGi defines a number of different service factories that have different behaviors.

Ordinarily services published into the registry are shared between all bundles. OSGi R6 adds a service.scope property, and uses the singleton value to indicate that the same instance is shared between all bundles. Services that are not factories will have this value.

Service factories allow multiple instances to be created, and these are the following three different types:

  • ServiceFactory: This creates a new instance per bundle (registered with service.scope=bundle in OSGi R6)
  • ManagedServiceFactory: This uses Config Admin to create instances per configuration/PID (registered with service.scope=bundle in OSGi R6)
  • PrototypeServiceFactory: This allows multiple instances per bundle (newly added in OSGi R6 registered with service.scope=prototype)

The ServiceFactory interface was added to allow a per-client bundle instance to be created, to avoid bundles sharing state. When a client bundle requests a service, if the bundle has already requested the service, then the same instance is returned; if not, a service is instantiated. When the client bundle goes away, so does the associated service instance.

A ManagedServiceFactory interface provides a means to instantiate multiple services instead of a single service per component. The EmptyFeedParser example is a configured singleton. If the configuration file exists, a service is registered; if not, no service is registered (multiple instances of a service can be created, each with their own configuration using service.pid-somename.cfg). Each bundle shares the instances of these services, but other client bundles will instantiate their own. Like ServiceFactory, if the service has been requested before, the same bundle will be returned.

The PrototypeServiceFactory interface was added in OSGi R6 (available in Eclipse Luna and later) as a means to provide a bundle with multiple instances of the same service. Instead of caching the previously delivered service per bundle, a new one is instantiated each time it is looked up. The client code can use BundleContext.getServiceObjects(ref).getService() to acquire a service through the PrototypeServiceFactory interface. This allows stateful services to be created.

Creating the EchoServer class

As an example, consider an EchoServer class that listens on a specific ServerSocket port. This can be run on zero or many ports at the same time. This code will be used by the next section, and simply creates a server running on a port and sets up a single thread to accept client connections and reflect what is typed. The code here is presented without explanation (other than of its purpose), and will be used to create multiple instances of this service in the next section.

When this is instantiated on a port (for example, when new EchoServer(1234) is called), it will be possible to telnet to the localhost on port 1234 and have content reflected as it is typed. To close the stream, use Ctrl + ] and then type close. The code is as follows:

public class EchoServer implements Runnable {
  private ServerSocket socket;
  private boolean running = true;
  private Thread thread;
  public EchoServer(int port) throws IOException {
    this.socket = new ServerSocket(port);
    this.thread = new Thread(this);
    this.thread.setDaemon(true);
    this.thread.start();
  }
  public void run() {
    try {
      byte[] buffer = new byte[1024];
      while (running) {
        Socket client = null;
        try {
          client = socket.accept();
          InputStream in = client.getInputStream();
          OutputStream out = client.getOutputStream();
          int read;
          while (running && (read = in.read(buffer)) > 0) {
            out.write(buffer, 0, read);
            out.flush();
          }
        } catch (InterruptedIOException e) {
          running = false;
        } catch (Exception e) {
        } finally {
          safeClose(client);
        }
      }
    } finally {
      safeClose(socket);
    }
  }
  public void safeClose(Closeable closeable) {
    try {
      if (closeable != null) {
        closeable.close();
      }
    } catch (IOException e) {
    }
  }
  public void stop() {
    running = false;
    this.thread.interrupt();
  }
}

Creating an EchoServiceFactory class

Create an EchoServiceFactory class that implements ManagedServiceFactory, and register it in the Activator class as before:

public void start(BundleContext context) throws Exception {
  … 
  properties = new Hashtable<String, String>();
  properties.put(Constants.SERVICE_PID,
   EchoServiceFactory.class.getName());
  context.registerService(ManagedServiceFactory.class,
   new EchoServiceFactory(), properties);
}

The EchoServiceFactory class is responsible for managing the children that it creates, and since they will be using threads, to appropriately stop them afterwards. The ManagedServiceFactory interface has three methods; getName, which returns a name of the service, and updated and deleted methods for reacting to configurations coming and going. To track them, create an instance variable in the EchoServiceFactory class called echoServers, which is a map of pid to EchoServer instances:

public class EchoServiceFactory implements ManagedServiceFactory {
  private Map<String, EchoServer> echoServers =
   new TreeMap<String, EchoServer>();
  public String getName() {
    return "Echo service factory";
  }
  public void updated(String pid, Dictionary<String, ?> props)
   throws ConfigurationException {
  }
  public void deleted(String pid) {
  }
}

The updated method will do two things; it will determine whether a port is present in the properties, and if so, instantiate a new EchoServer on the given port. If not, it will deconfigure the service:

public void updated(String pid, Dictionary<String, ?> properties)
 throws ConfigurationException {
  if (properties != null) {
    String portString = properties.get("port").toString();
    try {
      int port = Integer.parseInt(portString);
      System.out.println("Creating echo server on port " + port);
      echoServers.put(pid, new EchoServer(port));
    } catch (Exception e) {
      throw new ConfigurationException("port",
       "Cannot create a server on port " + portString, e);
    }
  } else if (echoServers.containsKey(pid)) {
    deleted(pid);
  }
}

If an error occurs while creating the service (because the port number isn't specified, isn't a valid integer, or is already in use), an exception will be propagated back to the runtime engine, which will be appropriately logged.

The deleted method removes it if present, and stops it:

public void deleted(String pid) {
  System.out.println("Removing echo server with pid " + pid);
  EchoServer removed = echoServers.remove(pid);
  if (removed != null) {
    removed.stop();
  }
}

Configuring EchoService

Now that the service is implemented, how is it configured? Unlike singleton configurations, the ManagedServiceFactory expects the value of pid to be a prefix of the name, followed by a dash (-), and then a custom suffix.

Ensure that the feeds.ui bundle is started, and that EchoServiceFactory is registered and waiting for configurations to appear:

osgi> ss | grep feeds.ui
27 ACTIVE com.packtpub.e4.advanced.feeds.ui_1.0.0.qualifier
osgi> start 27
osgi> bundle 27 | grep service.pid
{org.osgi.service.cm.ManagedService}={service.id=236,
 service.pid=com.packtpub.e4.advanced.feeds.ui}
{org.osgi.service.cm.ManagedService}={service.id=237,
 service.pid=com.packtpub.e4.advanced.feeds.ui.EmptyFeedParser}
{org.osgi.service.cm.ManagedServiceFactory}={service.id=238,
service.pid=com.packtpub.e4.advanced.feeds.ui.EchoServiceFactory}

Now create a configuration file in the Felix install directory /tmp/config/com.packtpub.e4.advanced.feeds.ui.EchoServiceFactory.cfg with the content port=1234. Nothing happens.

Now rename the file to something with a extension at the end, such as -1234, for example, /tmp/config/com.packtpub.e4.advanced.feeds.ui.EchoServiceFactory-1234.cfg. The suffix can be anything, but conventionally naming it for the type of instance being created (in this case, a service listening on port 1234) makes it easier to keep track of the services. When this happens, a service will be created:

Creating new echo server on port 1234

Telnetting to this port can see the output being returned:

$ telnet localhost 1234
Connected to localhost.
Escape character is '^]'.
hello
hello
world
world
^]
telnet> close
Connection closed by foreign host.

Creating a new service PID will start a new service; create a new file called /tmp/config/com.packtpub.e4.advanced.feeds.ui.EchoServiceFactory-4242.cfg with the content port=4242. A new service should be created:

Creating new echo server on port 4242

Test this by running telnet localhost 4242. Does this echo back content?

Finally, remove the service configuration for port 1234. This can be done by either deleting the configuration file, or simply renaming it with a different extension:

Removing echo server

Verify that the service has stopped:

$ telnet localhost 1234
Trying 127.0.0.1...
telnet: unable to connect to remote host

Tip

FileInstall only looks at *.cfg files, so renaming it to *.cfg.disabled has the same effect as deleting it, while making it easy to restore it subsequently.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset