Reader small image

You're reading from  Eclipse Plug-in Development Beginner's Guide - Second Edition

Product typeBook
Published inAug 2016
Reading LevelExpert
Publisher
ISBN-139781783980697
Edition2nd Edition
Languages
Tools
Right arrow
Author (1)
Alex Blewitt
Alex Blewitt
author image
Alex Blewitt

contacted on 30 aug 16 _____________ Dr Alex Blewitt has over 20 years of experience in Objective-C and has been using Apple frameworks since NeXTstep 3.0. He upgraded his NeXTstation for a TiBook when Apple released Mac OS X in 2001 and has been developing on it ever since. Alex currently works for an investment bank in London, writes for the on-line technology news site InfoQ and has published two other books for Packt publishing. He also has a number of apps on the Apple AppStore through Bandlem Limited. When he's not working on technology, and if the weather is nice, he likes to go flying from the nearby Cranfield airport. Alex writes regularly at his blog, http://alblue.bandlem.com, as well tweeting regularly from Twitter as @alblue. Acknowledgements This book would not have been possible without the ongoing love and support of my wife Amy, who has helped me through both the highs and lows of life. She gave me the freedom to work during the many late nights and weekends that it takes to produce a book and its associated code repository. She truly is the Lem of my life. I'd also like to thank my parents, Ann and Derek, for their encouragement and support during my formative years. It was this work ethic that allowed me to start my technology career as a teenager and to incorporate my first company before I was 25. I'd also like to congratulate them on their 50th wedding anniversary in 2015, and I look forward to reaching that goal with Amy. Thanks are due especially to the reviewer of this version of the book: Antonio Bello, as well as the previous version of this book: Nate Cook, James Robert and Arvid Gerstmann, who provided excellent feedback on the contents of this book during development and caught many errors in both the text and code. Any remaining errors are my own. I'd also like to thank my children Sam and Holly for inspiring me and hope that they too can achieve anything that they set their minds to. Finally, I'd like to thank Ben Moseley and Eren Kotan, both of whom introduced me to NeXT in the first place and set my career going on a twenty year journey to this book.
Read more about Alex Blewitt

Right arrow

Appendix A. Using OSGi Services to Dynamically Wire Applications

This appendix will present OSGi services as a means of communicating and connecting applications. Unlike the Eclipse extension point mechanism, OSGi services can have multiple versions available at runtime and can work in other OSGi environments, such as Felix or other commercial OSGi runtimes.

Services overview


In an Eclipse or OSGi runtime, each individual bundle is its own separate module, which has explicit dependencies on library code via Import-Package, Require-Bundle, or Require-Capability. These express static relationships and provide a way of configuring the bundle's classpath.

However, this presents a problem. If services are independent, how can they use contributions provided by other bundles? In Eclipse's case, the Extension Registry provides a means for code to look up providers. In a standalone OSGi environment, OSGi services provide a similar mechanism.

A service is an instance of a class that implements a service interface. When a service is created, it is registered with the services framework under one (or more) interfaces, along with a set of properties. Consumers can then get the service by asking the framework for implementers of that specific interface.

Note

Services can also be registered under an abstract class, but this is not recommended. Providing a service interface exposed as an abstract class can lead to unnecessary coupling of client to implementation.

This separation allows the consumer and producer to depend on a common API bundle, but otherwise be completely disconnected from one another. This allows both the consumer and producer to be mocked out or exchanged with different implementations in the future.

Registering a service programmatically


To register a service, an instance of the implementation class needs to be created and registered with the framework. Interactions with the framework are performed with a BundleContext—typically provided in the BundleActivator.start method and stored for later use, or resolved dynamically from FrameworkUtil.getBundle().getBundleContext().

Creating an activator

A bundle's activator is a class that is instantiated and coupled to the lifetime of the bundle. When a bundle is started, if a manifest entry Bundle-Activator exists, then the corresponding class is instantiated. As long as it implements the BundleActivator interface, the start method will be called. This method is passed an instance of BundleContext, which is the bundle's connection back to the hosting OSGi framework.

Note

Although creating an activator may seem a simple way of registering a service, it is better to use declarative services, which are covered later in this appendix.

Create a new plug-in project com.packtpub.e4.timezones, to host a service for supplying ZoneId instances. Create a class called com.packtpub.e4.timezones.internal.TimeZonesActivator that implements the org.osgi.framework.BundleActivator interface.

Add the Bundle-Activator and Import-Package to the META-INF/MANIFEST.MF file as follows:

Import-Package: org.osgi.framework
Bundle-Activator:
  com.packtpub.e4.timezones.internal.TimeZonesActivator

Now when the bundle is started, the framework will automatically invoke the start method of the TimeZonesActivator, and correspondingly the stop method when the bundle is stopped. Test this by inserting a pair of println calls:

public class TimeZonesActivator implements BundleActivator {
  public void start(BundleContext context) throws Exception {
    System.out.println("Bundle started");
  }
  public void stop(BundleContext context) throws Exception {
    System.out.println("Bundle stopped");
  }
}

Now run it as an OSGi framework by going to Run | Run Configurations… | OSGi Framework. Ensure that all the workspace and target bundles are deselected with the Deselect all button. Add the com.packtpub.e4.timezones bundle, the Equinox console org.eclipse.equinox.console and the gogo shell org.apache.felix.gogo.shell. Ensure that the Include optional dependencies checkbox is not selected and click on Add Required Bundles, to build a list of the required steps.

The required bundles are:

  • com.packtpub.e4.timezones

  • org.apache.felix.gogo.runtime

  • org.apache.felix.gogo.shell

  • org.eclipse.equinox.console

  • org.eclipse.osgi

On the console, when the bundle is started (which happens automatically if the Default Auto-Start is set to true), the Bundle started message should be seen.

Note

If the bundle does not start, ss in the console will print a list of bundles and start 2 will start bundle with the id 2. Afterwards, stop 2 can be used to stop bundle 2. Bundles can be stopped/started dynamically in an OSGi framework.

Registering a service

Once the TimeZonesActivator is created, a BundleContext will be available for interaction with the framework. This can be persisted for subsequent use in an instance field, but can also be used directly to register a service.

The BundleContext provides a method called registerService, which takes an interface, an instance, and an optional Dictionary of key/value pairs. This can be used to register instances of the timezone provider with the runtime.

A TimeZonesService interface needs to be provided, along with an implementation. Create an interface called TimeZonesService in the com.packtpub.e4.timezones package as follows:

package com.packtpub.e4.timezones;
import java.time.ZoneId;
import java.util.Map;
import java.util.Set;

public interface TimeZonesService {
  public Map<String, Set<ZoneId>> getTimeZones();
}

Create an implementation in the TimeZonesProvider class, using the code developed in the Creating an Empty TimeZone View section of Chapter 2, Creating Views with SWT, in the com.packtpub.e4.timezones.internal package as follows:

public class TimeZonesProvider implements TimeZonesService {
  public Map<String, Set<ZoneId>> getTimeZones() {
    Supplier<Set<ZoneId>> sortedZones =
     () -> new TreeSet<>(new TimeZoneComparator());
    return ZoneId.getAvailableZoneIds().stream() // stream
     .filter(s -> s.contains("/")) // with / in them
     .map(ZoneId::of) // convert to ZoneId
     .collect(Collectors.groupingBy( // and group by
      z -> z.getId().split("/")[0], // first part
      TreeMap::new, Collectors.toCollection(sortedZones)));
  }
} 

The TimeZonesComparator allows time zones to be compared by their ID:

public class TimeZonesComparator implements Comparator<ZoneId> {
  @Override
  public int compare(ZoneId o1, ZoneId o2) {
    return o1.getID().compareTo(o2.getID());
  }
}

Now that the provider is available, it can be registered as a service in the TimeZonesActivator method start as follows:

public void start(BundleContext context) throws Exception {
  context.registerService(TimeZonesService.class,
   new TimeZonesProvider(), null);
}

Now start the framework again. In the console that is launched, look for the result corresponding to the timezones bundle:

osgi> bundles | grep timezones
com.packtpub.e4.timezones_1.0.0.qualifier [5]
  {com.packtpub.e4.timezones.TimeZonesService}={service.id=42}

This shows that the bundle 5 has started a service, using the interface com.packtpub.e4.timezones.TimeZonesService, and with service ID 42.

It is also possible to query the runtime framework for services of a known interface type directly using the services command and an LDAP style filter:

osgi> services
  "(objectClass=com.packtpub.e4.timezones.TimeZonesService)"

{com.packtpub.e4.timezones.TimeZonesService}={service.id=42}
  "Registered by bundle:"
    com.packtpub.e4.timezones_1.0.0.qualifier [5]
  "No bundles using service."

The results displayed represent the service instantiated by the timezones bundle. It can be introspected using the service command by passing the service.id:

osgi> service 42
TimeZones [Africa=[Africa/Abidjan, Africa/Accra, Africa/...

Priority of services

Services have an implicit order, based on the order in which they were instantiated. Each time a service is registered, a global service.id is incremented.

It is possible to define an explicit service ranking with an integer property. This is used to ensure relative priority between equivalent services, regardless of the order in which they are registered. For services with equal service.ranking values, the service.id values are compared.

Note

OSGi R6 provides an additional property, service.bundleid, which is used to denote the id of the bundle that provides the service. This is not used to order services and is for informational purposes only.

To pass a priority into the service registration, create a helper method called priority that takes an int and returns a Hashtable with the key service.ranking using that value. This can be used to pass a priority into the service registration methods:

public class TimeZonesActivator implements BundleActivator {
  public void start(BundleContext context) throws Exception {
    context.registerService(TimeZonesService.class,
     new TimeZonesProvider(), priority(1));
  }
  private Dictionary<String, Object> priority(int priority) {
    Hashtable<String, Object> dict = new Hashtable<>();
    dict.put("service.ranking", Integer.valueOf(priority));
    return dict;
  }
}

Now when the framework starts, the services are displayed in priority ordering:

osgi> services | grep timezones
{com.packtpub.e4.timezones.TimeZonesService}=
 {service.ranking=1, service.id=42, service.bundleid=5,
  service.scope=singleton}
"Registered by bundle:" 
 com.packtpub.e4.timezones_1.0.0.qualifier [5]

Tip

Dictionary was the original Java Map interface, and Hashtable the original HashMap implementation. They fell out of favor in Java 1.2 when Map and HashMap were introduced (mainly because they weren't synchronized by default), but OSGi was developed to run on early releases of Java (JSR 8 proposed adding OSGi as a standard for the Java platform back in 1999). Not only that, early low-powered Java mobile devices didn't support the full Java platform, instead exposing the original Java 1.1 data structures. Because of this history, many APIs in OSGi refer to only Java 1.1 data structures, so that low-powered devices can still run OSGi systems.

Using the services

The BundleContext can be used to acquire services as well as register them.

One way of obtaining an instance of BundleContext is to store it in the TimeZonesActivator.start method as a static variable. That way, classes elsewhere in the bundle will be able to acquire the context, along with an accessor method. A better way is to use FrameworkUtil.getBundle(class).getBundleContext() instead.

A TimeZonesFactory can be created to acquire the TimeZoneService. OSGi services are represented via a ServiceReference (which is a sharable object representing a handle to the service); this in turn can be used to acquire a service instance:

public class TimeZonesFactory {
  private static final Bundle bundle =
   FrameworkUtil.getBundle(TimeZonesService.class);
  private static final BundleContext context =
   bundle.getBundleContext();
  private static final ServiceReference<TimeZonesService> sr =
   context.getServiceReference(TimeZonesService.class);
}

The service instance is acquired with the context.getService(ServiceReference) call. The contract is that the caller 'borrows' the service, and when finished should return it with an ungetService(ServiceReference) call. This can be wrapped in a block taking a Consumer<TimeZonesService> (which makes it easy to use with lambdas):

public static void use(Consumer<TimeZonesService> consumer) {
  TimeZonesService service = context.getService(sr);
  try {
    consumer.accept(service);
  } finally {
    context.ungetService(sr);
  }
}

Technically the service is only supposed to be used between the getService and ungetService call as its lifetime may be invalid afterwards; instead of returning a service references, the common pattern is to pass in a unit of work that accepts the service and call the ungetService afterwards.

Note

Note that the preceding example requires better error handling, such as checking for null and responding appropriately to the caller. This is left as an exercise for the reader.

Lazy activation of bundles

By default bundles are not started when they are accessed for the first time. If the bundle needs its activator to be called prior to using any of the classes in the package, it needs to be marked as having an activation policy of lazy. This is done by adding the following entry to the MANIFEST.MF file:

Bundle-ActivationPolicy: lazy

The manifest editor can be used to add this configuration line, by selecting Activate this plug-in when one of its classes is loaded.

Comparison of services and extension points

Both mechanisms (using the extension registry and using the services) allow for a list of services to be contributed and used by the application. What are the differences between them and are there any advantages to one or the other?

Both the registry and services approaches can be used outside of an Eclipse runtime. They work the same way when used in other OSGi implementations (such as Felix) and can be used interchangeably. The registry approach can also be used outside of OSGi, although that is far less common.

The registry encodes its information in the plugin.xml file by default, which means that it is typically edited as part of a bundle's install (it is possible to create registry entries from alternative implementations if desired, but this rarely happens). The registry has a notification system that can listen to contributions being added and removed.

The services approach uses the OSGi framework to store and maintain a list of services. These don't have an explicit configuration file and in fact can be contributed by code (such as the registerService calls previously covered) or by declarative representations (which are covered in the next section).

The separation of how the service is created versus how the service is registered is a key difference between this and the registry approach. Like the registry, the OSGi services system is able to generate notifications when services come and go.

One key difference in an OSGi runtime is that bundles depending on the Eclipse registry must be declared as singletons; that is, they have to use the ;singleton:=true directive on the Bundle-SymbolicName. This means there can only be one version of a bundle that exposes registry entries in a runtime, as opposed to multiple versions in the general services case.

While the registry does provide mechanisms to be able to instantiate extensions from factories, these typically involve simple configurations and/or properties that are hard-coded in the plugin.xml files themselves, which would not be appropriate for storing sensitive details such as passwords. On the other hand, a service can be instantiated from whatever external configuration information is necessary and then registered, such as a JDBC connection for a database.

Finally extensions in the registry are declarative by default and are activated on demand. This allows Eclipse to start up quickly as it does not need to build the full set of class loader objects or run code, and then bring up services on demand. Although the approach previously didn't use declarative services, it is possible to do this as covered in the next section.

Registering a service declaratively


Registering services imperatively in the start method of an Activator method is one way of installing services in an OSGi framework. However it requires that the bundle be started, which requires that either the bundle is started automatically or has classes (such as API classes) accessed by default. Both approaches will mean that additional code has to run to bring the system into the desired state.

An alternative is to use one of the declarative service approaches, which represent the service definition in an external file. These are processed using an extender pattern, which looks out for bundles with a given file or files and then instantiates the service from this definition. It combines the declarative nature of the extension registry with the flexibility of OSGi services.

There are two providers of declarative service support, which both achieve a similar result but use slightly different configuration files and approaches. They are Declarative Services and Blueprint. Since the most common use case for Eclipse plug-ins is to use Declarative Services, they will be covered here (Blueprint is covered in Mastering Eclipse Plug-in Development by the same author).

Declarative Services

Declarative Services, or simply DS, was the original declarative implementation for instantiating services in a declarative fashion in an OSGi runtime. Both Equinox and Felix have DS modules, and it is a required part of the Eclipse 4 runtime, so can be trivially expected to be present. In the OSGi specification, it is referred to as the Services Component Runtime (SCR), which is why the associated package names use org.osgi.service.component.

The DS bundle needs to be started before it can process bundles; as a result, it is typically started early on. It listens to bundles being installed and then looks for a specific header in the META-INF/MANIFEST.MF:

Service-Component: OSGI-INF/*.xml

If the DS bundle finds this header, it looks for files contained in the bundle itself matching the file pattern specified. This is a comma-separated list, and can use a single wildcard * character (which will match file names but not directories).

The service document is then loaded and parsed, and used to instantiate and register services with the OSGi runtime environment. The XML document uses namespaces to represent the component, using http://www.osgi.org/xmlns/scr/v1.2.0. Different versions of SCR use different endings; v1.0.0 is defined as the first version, with v1.1.0 the second. The current version (as of the writing of this book) uses the suffix v1.3.0.

Each service document defines a single service, which has an implementation class as well as an identifier. The service can be registered under one or more interfaces, as well as optional priorities.

This can be used to remove the custom code in the TimeZonesActivator created previously, by deleting the class and removing the Bundle-Activator from the manifest.

If the application is run now, the service won't be registered. To register these as OSGi services declaratively, create a file called OSGI-INF/timezones.xml:

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
 name="TimeZonesProvider">
 <implementation
  class="com.packtpub.e4.timezones.internal.TimeZonesProvider"/>
  <service>
   <provide
    interface="com.packtpub.e4.timezones.TimeZonesService"/>
  </service>
</scr:component>

Tip

Don't forget to tell Eclipse to consider this part of the build by adding OSGI-INF/ to the build.properties file in the bin.includes property.

As long as a declarative services provider is installed in the application and started, the service will be created on demand.

Note

Client bundles should be able to express a dependency on a Declarative Services provider by using the following requirement in the manifest:

Require-Capability:
 osgi.extender;osgi.extender="osgi.component"

However, this is not yet implemented in Eclipse.

The launch configuration can detect whether or not declarative services are installed with the Validate Bundles button, but at the moment the Add Required Bundles button does not resolve the problem. At present the org.eclipse.equinox.ds bundle must be resolved manually to fix this problem. It will also require org.eclipse.equinox.util and org.eclipse.osgi.services to be added to the launch configuration.

Properties and Declarative Services

Declarative Services can also be used to register properties with the service when it is registered. These properties can be sourced either from the services XML file or an external properties file.

To add the service.ranking property to the registered service, add the following into the services document:

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
 name="TimeZonesProvider">
  ...
  <property name="service.ranking" type="Integer" value="2"/>
</scr:component>

When the application is restarted, the services console command will show that the service.ranking property is associated with the TimeZonesService:

osgi> services | grep timezones
{com.packtpub.e4.timezones.TimeZonesService}=
 {service.ranking=2,
  component.name=TimeZonesProvider,
  component.id=0,
  service.id=48,
  service.bundleid=11,
  service.scope=bundle}

Tip

If the property isn't listed, add a -clean argument to the Eclipse runtime console; sometimes the files are cached and PDE doesn't always notice when files are changed.

The property types can be one of:

  • String (default)

  • Long

  • Double

  • Float

  • Integer

  • Byte

  • Character

  • Boolean

  • Short

Additionally, arrays of elements can be specified by placing them in the body of the element instead of as an attribute:

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
 name="TimeZonesProvider">
  ...
  <property name="compass.point" type="String">
    NORTH
    EAST
    SOUTH
    WEST
  </property>
</scr:component>

Service references in Declarative Services

As well as hard-coded values, it is also possible to set up references to services in DS. The service implementation can have bind and unbind methods, which are called when a service becomes available or goes away.

These can be mandatory or optional; if the dependency is mandatory then the service is not presented until its dependencies are available. If they are optional, the service can come up and be assigned later. They can also be single-valued or multi-valued. These are encoded in the relationships cardinality:

  • 0..1: This service is optional, either zero or one instance needed

  • 1..1: This service is mandatory, exactly one instance needed (default)

  • 0..n: This service is optional, may have zero or more instances

  • 1..n: This service is mandatory, may have one or more instances

This can be used to inject a LogService (from the org.osgi.service.log package; it may be necessary to add this as an imported package to the bundle) into the component. Modify the TimeZonesProvider to accept an instance of the LogService by adding a setLog and unsetLog method:

private LogService logService;
public void setLog(LogService logService) {
  this.logService = logService;
}
public void unsetLog(LogService logService) {
  this.logService = null;
}

This can be used to report on how many time zones are loaded in the getTimeZones method of the TimeZonesProvider class:

if (logService != null) {
  logService.log(LogService.LOG_INFO,
   "Time zones loaded with " + timeZones.size());
}

To configure DS to provide a log service, the following must be added to the timezones.xml file:

<scr:component name="TimeZonesProvider"
 xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0">
 ...
 <reference interface="org.osgi.service.log.LogService"
  cardinality="0..1" name="log" 
  bind="setLog" unbind="unsetLog"/>
</scr:component>

This tells DS that the log service is optional (so it will bring the service up before a LogService is available), and that setLog(log) will be called when it is available. DS also provides an unbind method which can be used to remove the service if it goes away. The instance is provided for both the setLog and unsetLog method, which may look strange—but when setting multiple elements the methods are typically called addThing and removeThing, which may be more appropriate.

Multiple components and debugging Declarative Services

Although this seems to imply that an XML file can only contain one component, in fact an XML parent element can be defined with multiple scr namespaced children. Since all elements outside the scr namespace are ignored, it is possible to embed an XHTML document with an scr namespaced element inside, and still have it picked up by Declarative Services:

<xhtml>
  <h1>Example HTML file with SCR elements</h1>
  <h2>Component One</h2>
  <scr:component name="One" xmlns:scr="http://...">
    …
  </scr:component>
  <h2>Component Two</h2>
  <scr:component name="Two" xmlns:scr="http://...">
    …
  </scr:component>
</xhtml>

Note that many developers will use a one-to-one mapping between service components and corresponding XML files; it is rare to see a single XML file with multiple service components. It is recommended to only put one component per XML file for ease of maintenance.

Tip

When using DS inside Equinox, using -Dequinox.ds.print=true gives additional diagnostic information on the state of the declarative services, including highlighting what services are waiting. For Felix, specifying -Dds.showtrace=true can increase logging, as can -Dds.loglevel=4.

Dynamic Service annotations

Although XML allows for flexibility, it has fallen out of fashion in the Java community in favor of Java annotations. The 1.2 version of the OSGi DS specification provides annotations that can be used to mark the code such that a build time processor can create the service component XML files automatically.

Tip

Note that the standard OSGi annotations are not read at runtime by the service but only build-time tools such as maven-scr-plugin. As a result they should be optionally imported, since they aren't needed at runtime, or with compile scope if using a Maven-based build.

To use the annotations, add the following as an Import-Package for the bundle in the MANIFEST.MF:

Import-Package:
 org.osgi.service.component.annotations;
 version="1.2.0";
 resolution:=optional

The @Component annotation can now be added to the individual classes that should be represented as services. Add this to the TimeZonesProvider:

import org.osgi.service.component.annotations.Component;
@Component(name="TimeZonesProvider2",
 service={TimeZonesService.class},
 property={"service.ranking:Integer=1"})
public class TimeZonesProvider implements TimeZonesService {
  ...
}

Processing annotations at Maven build time

If using Maven Tycho to build bundles, it is possible to add a Maven plug-in to generate the service xml files from components. Maven Tycho is covered in more detail in Chapter 12, Automated Builds with Tycho.

To configure the maven-scr-plugin to a build, first add the following dependency to the pom.xml file:

<dependencies>
  <dependency>
    <groupId>org.apache.felix</groupId>
    <artifactId>org.apache.felix.scr.ds-annotations</artifactId>
    <version>1.2.8</version>
    <scope>compile</scope>
  </dependency>
</dependencies>

This both provides the org.osgi.service.component.annotations classes as well as the processing engine necessary to generate the components. Note that even if other dependencies are given (say, osgi.enterprise or equinox.ds) this isn't sufficient on its own to generate the service.xml files.

Next the plugin needs to be added to the pom.xml file:

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.felix</groupId>
      <artifactId>maven-scr-plugin</artifactId>
      <version>1.22.0</version>
      <configuration>...</configuration>
      <executions>...</executions>
    </plugin>
  </plugins>
  <sourceDirectory>src</sourceDirectory>
</build>

The source directory needs to be specified to match the value of the source attribute of the build.properties file (which is used by eclipse-plugin instead of sourceDirectory), as otherwise the maven-scr-plugin cannot find the source files.

The plug-in needs to be configured specifically for eclipse-plugin projects. Firstly, the supported projects default to jar and bundle for the maven-scr-plugin, so it needs to be given additional configuration to permit processing eclipse-plugin projects.

Secondly the service files are written to target/classes/ by default. Although this will work, it makes for more difficult debugging in Eclipse. Instead, the maven-scr-plugin can be configured to write it to the project root, which will place the service files under OSGI-INF. This permits the code to be tested in Eclipse as well as exported using the standard build tools:

<configuration>
  <supportedProjectTypes>
    <supportedProjectType>eclipse-plugin</supportedProjectType>
  </supportedProjectTypes>
  <outputDirectory>${basedir}</outputDirectory>
</configuration>

Finally, to hook it in with the standard build process, add the following to the build:

<executions>
  <execution>
    <id>generate-scr</id>
    <goals>
      <goal>scr</goal>
    </goals>
  </execution>
</executions>

Now when the package is built, the service descriptor xml file will be automatically be regenerated based on the annotations. The file name is derived from the class name.

Dynamic services


The OSGi specification defines four different layers:

  • Security Layer: In this layer, all actions are checked against a security permissions model

  • Module Layer: In this layer, modules are specified as bundles that have dependencies

  • Life Cycle Layer: This layer bundles coming and going and firing events

  • Service Layer: In this layer, dynamic services that come and go

The Services layer allows bundles to communicate by defining an API that can cross bundle layers. However, the services layer also allows the services to come and go dynamically, instead of being fixed at runtime.

This mechanism allows services to be exported over a network, and since the network can come and go (as can the remote endpoint) the OSGi services layer can replicate that same functionality.

Responding to services dynamically coming and going may add a slight difficulty to the client code, but it will be more robust in case of failure. The following sections will present different ways of achieving dynamism in services.

Resolving services each time

The easiest way of working with dynamic services is to list the services each time they are needed. The example so far uses this technique to allow different services to be contributed.

This technique can work if the list of services is infrequently needed. However each time the lookup is performed, there is a cost to the acquisition, which may not be desirable.

Using a ServiceTracker

The OSGi framework provides a ServiceTracker class which can be used to simplify the acquisition of one or more services in a standard way. Provided in the org.osgi.util.tracker package, the ServiceTracker class has a constructor that takes a class and a BundleContext object, along with an optional filter specification.

Tip

The ServiceTracker has an open method which must be called prior to use, as otherwise it will not return any services.

Add the package to the timezones bundle's manifest as an import:

Import-Package: org.osgi.util.tracker

Modify the TimeZonesFactory so that a ServiceTracker is acquired in a static initializer, and that open is called. This simplifies the use method to simply delegate to the service tracker:

public class TimeZonesFactory {
  private static final Bundle bundle =
    FrameworkUtil.getBundle(TimeZonesService.class);
  private static final BundleContext context =
    bundle.getBundleContext();
  private static final ServiceTracker<TimeZonesService,
    TimeZonesService> tracker = 
     new ServiceTracker<>(context,TimeZonesService.class, null);
  static {
    tracker.open(); // Remember to call this!
  }
  public static void use(Consumer<TimeZonesService> consumer) {
    consumer.accept(tracker.getService());
  }
}

The ServiceTracker also has a close method, which should be called when services are no longer required to be tracked.

Tip

Generally tying the service tracker's lifecycle to another lifecycle is more appropriate, as otherwise this can leak implementation.

Filtering services

The service tracker, as it is currently implemented, returns all compatible services that implement the interface (if true is passed to the open call, both compatible and incompatible services are returned; this should generally not be used).

It is also possible to use a filter to restrict the list of services that are returned. OSGi filters are specified using the LDAP filter syntax, which uses prefix notation and parentheses to group elements. Here is how to read it:

  • A and B: (&(A)(B))

  • A or B: (|(A)(B))

  • Not A: (!(A))

  • A equals B: (A=B)

  • A contains B: (A=*B*)

These can be nested to form complex queries.

The services command in the Equinox console allows a filter to be evaluated. Each service is published into the registry, and the filter objectClass= allows services matching a particular interface to be found, as was done earlier in the chapter:

osgi> services "(objectClass=*.TimeZonesService)"
{com.packtpub.e4.timezones.TimeZonesService}={service.ranking=2,
 component.name=TimeZonesProvider, component.id=0, service.id=48}

It's possible to filter on other properties as well. For example, DS registers a component.id property with a service, so this can be used to create a filter for just DS registered components:

osgi> services "(&(objectClass=*.TimeZonesService)(component.id=*))"
{com.packtpub.e4.timezones.TimeZonesService}={service.ranking=2,
 component.name=TimeZonesProvider, component.id=0, service.id=48}

This looks for services ending in TimeZonesService and which have a value for the component.id property.

Filters can be included in the ServiceTracker to ensure that only desired services are picked up. For example, to include only services that aren't registered by DS, the following can be coded into the ServiceTracker:

Filter filter = context.createFilter(
 "(&(objectClass=*.TimeZonesService)(!(component.id=*)))");
st = new ServiceTracker<TimeZonesService, TimeZonesService>(
 context, filter, null);
st.open();

Note

This may be useful to enable debugging; the filter could be overridden by a system property, for example. Note that the createFilter method throws a checked syntax exception if it is invalid, which must be handled in the code.

Obtaining a BundleContext without using an activator

Since the ServiceTracker needs the BundleContext to register a listener, it is conventional to set up a BundleActivator for the sole purpose of acquiring a BundleContext.

Since this incurs a performance penalty, using a different mechanism to acquire the context will speed the start-up process. Fortunately there is a class, FrameworkUtil, that can be used to acquire a Bundle for any given class, and from there, the BundleContext. This allows the implementation of the TimeZonesActivator to be removed:

BundleContext context = FrameworkUtil.
  getBundle(TimeZonesFactory.class).getBundleContext();

Using this mechanism adds no performance penalty and should be used in favor of a global static instance to the BundleContext. It also potentially allows the bundle's activator to be removed from the bundle.

Note

If the bundle is not started, it will not have a BundleContext and so the returned value here may be null. Code should defensively handle this case.

Dependent Services

It is fairly common that an OSGi service depends on other OSGi services. As such it can help if the services are set up and made available when the bundles are available. The Declarative Services approach can be used to register services on demand when the requirements are satisfied.

For DS, when the cardinality of the relationship is not optional (in other words, the relationship is 1..1 or 1..n), then the service won't be started until the required dependent services are available. For example, a menu service may not be required until the graphical user interface service is present; and services that wish to contribute to the menu service won't be able to work until the menu service is present.

Delaying the creation of the services until they are needed will result in shorter start-up times of the application.

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 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 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. The 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. Search for org.apache.felix.fileinstall at http://search.maven.org and download the latest Jar. It can be imported into Eclipse as a plug-in project with File | Import | Plug-in Development | Plug-ins and Fragments to enable it to run in a test runtime.

The system property felix.fileinstall.dir must be specified to use FileInstall. It defaults to ./load from the current working directory, but for the purpose 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 configuration later.

Tip

Make sure that FileInstall is configured to start when the runtime begins, so that it picks up configurations. This can be done by specifying the start level on the OSGi framework launch configuration page, or by using the console to verify that the bundle's state is ACTIVE.

Installing ConfigAdmin

To configure services, ConfigAdmin needs to be installed into the runtime as well. The two standard implementations of these are Felix ConfigAdmin 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. Search for org.apache.felix.configadmin at http://search.maven.org, download the latest Jar, and then import this as a plug-in project to Eclipse with 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 configurations 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, which can be used to respond to changes in configuration without stopping and restarting the component.

To configure the TimeZonesProvider with config admin, add a configure method that takes a Map of values. If it's non-null and there is a key max, then parse it as an int and use that as the max value. Use this to set a limit on the number of time zones returned in the getTimeZones method:

private long max = Long.MAX_VALUE;
public Map<String, Set<ZoneId>> getTimeZones() {
...
  .filter(s -> s.contains("/")) // with / in them
  .limit(max) // return this many only
  .map(ZoneId::of) // convert to ZoneId
...
}
public void configure(Map<String, Object> properties) {
  max = Long.MAX_VALUE;
  if (properties != null) {
    String maxStr = (String) properties.get("max");
    if (maxStr != null) {
      max = Long.parseLong(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="TimeZonesProvider">

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

Now when the application is run, the configuration should be loaded and configure the TimeZonesProvider, such that when the time zones are requested, it shows a maximum of one value.

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

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.

Service factories allow multiple instances to be created, and there are three different types:

  • ServiceFactory, which creates a new instance per bundle (registered with service.scope=bundle in OSGi R6)

  • ManagedServiceFactory, which uses config admin to create instances per configuration/pid (registered with service.scope=bundle in OSGi R6)

  • PrototypeServiceFactory, which allows multiple instances per bundle (newly added in OSGi R6 registered with service.scope=prototype)

The ServiceFactory allows 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 provides a means to instantiate multiple services instead of a single service per component. 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 was added in OSGi R6 (available since Eclipse Luna) as a means of providing 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. This allows stateful services to be created.

Creating the EchoService

As an example, consider an EchoServer 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 echo back what is typed. The code here is presented without explanation other than 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 echoed back as it is typed. To close the connection, use Ctrl + ] and then type close:

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

Create an EchoServiceFactory that implements ManagedServiceFactory, and register it as a managed service factory in a component:

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0"
 name="EchoServiceFactory">
 <implementation
  class="com.packtpub.e4.timezones.internal.EchoServiceFactory"/>
  <service>
   <provide
    interface="org.osgi.service.cm.ManagedServiceFactory"/>
  </service>
  <property name="service.pid" type="String"
   value="com.packtpub.e4.timezones.internal.EchoServiceFactory"/>
</scr:component>

The EchoServiceFactory is responsible for managing the children that it creates and, since they will be using threads, appropriately stopping them afterwards. The ManagedServiceFactory has three methods; getName, which returns the 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 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 if 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 the EchoServices

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

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

osgi> ss | grep timezones
13 ACTIVE com.packtpub.e4.timezones._1.0.0.qualifier
osgi> start 13
osgi> bundle 13 | grep service.pid
{org.osgi.service.cm.ManagedServiceFactory}={
service.pid=com.packtpub.e4.timezones.internal.EchoServiceFactory}

Now create a configuration file in the Felix install directory com.packtpub.e4.timezones.internal.EchoServiceFactory.cfg with the content port=1234. Nothing happens.

Now rename the file to something with an -extension on the end, such as -1234. 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. For example, create com.packtpub.e4.timezones.internal.EchoServiceFactory-1234.cfg with contents port=1234 in the configuration directory. 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 com.packtpub.e4.timezone.internal.EchoServiceFactory-4242.cfg with the contents 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 as well?

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

Note

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

Summary


This appendix looked at OSGi services as an alternative means of providing dependent services in an Eclipse or OSGi application. By registering services either imperatively at bundle startup in an activator, or by using one of the declarative services representations, an operational system can evolve by connecting services together in a single runtime. Different approaches for configuration were shown, with either embedded values in the service component document, or derived from external properties or configuration with config admin.

lock icon
The rest of the chapter is locked
You have been reading a chapter from
Eclipse Plug-in Development Beginner's Guide - Second Edition
Published in: Aug 2016Publisher: ISBN-13: 9781783980697
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at £13.99/month. Cancel anytime

Author (1)

author image
Alex Blewitt

contacted on 30 aug 16 _____________ Dr Alex Blewitt has over 20 years of experience in Objective-C and has been using Apple frameworks since NeXTstep 3.0. He upgraded his NeXTstation for a TiBook when Apple released Mac OS X in 2001 and has been developing on it ever since. Alex currently works for an investment bank in London, writes for the on-line technology news site InfoQ and has published two other books for Packt publishing. He also has a number of apps on the Apple AppStore through Bandlem Limited. When he's not working on technology, and if the weather is nice, he likes to go flying from the nearby Cranfield airport. Alex writes regularly at his blog, http://alblue.bandlem.com, as well tweeting regularly from Twitter as @alblue. Acknowledgements This book would not have been possible without the ongoing love and support of my wife Amy, who has helped me through both the highs and lows of life. She gave me the freedom to work during the many late nights and weekends that it takes to produce a book and its associated code repository. She truly is the Lem of my life. I'd also like to thank my parents, Ann and Derek, for their encouragement and support during my formative years. It was this work ethic that allowed me to start my technology career as a teenager and to incorporate my first company before I was 25. I'd also like to congratulate them on their 50th wedding anniversary in 2015, and I look forward to reaching that goal with Amy. Thanks are due especially to the reviewer of this version of the book: Antonio Bello, as well as the previous version of this book: Nate Cook, James Robert and Arvid Gerstmann, who provided excellent feedback on the contents of this book during development and caught many errors in both the text and code. Any remaining errors are my own. I'd also like to thank my children Sam and Holly for inspiring me and hope that they too can achieve anything that they set their minds to. Finally, I'd like to thank Ben Moseley and Eren Kotan, both of whom introduced me to NeXT in the first place and set my career going on a twenty year journey to this book.
Read more about Alex Blewitt