Book Image

Eclipse Plug-in Development Beginner's Guide - Second Edition

By : Alex Blewitt
Book Image

Eclipse Plug-in Development Beginner's Guide - Second Edition

By: Alex Blewitt

Overview of this book

Eclipse is used by everyone from indie devs to NASA engineers. Its popularity is underpinned by its impressive plug-in ecosystem, which allows it to be extended to meet the needs of whoever is using it. This book shows you how to take full advantage of the Eclipse IDE by building your own useful plug-ins from start to finish. Taking you through the complete process of plug-in development, from packaging to automated testing and deployment, this book is a direct route to quicker, cleaner Java development. It may be for beginners, but we're confident that you'll develop new skills quickly. Pretty soon you'll feel like an expert, in complete control of your IDE. Don't let Eclipse define you - extend it with the plug-ins you need today for smarter, happier, and more effective development.
Table of Contents (24 chapters)
Eclipse Plug-in Development Beginner's Guide Second Edition
Credits
Foreword
About the Author
Acknowledgments
About the Reviewers
www.PacktPub.com
Preface
Index

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.